在循环中用作 "const &" 函数参数的临时对象的编译器优化?
Compiler optimization for a temporary object used as a "const &" function argument in a loop?
我在下面有一个永远的线程循环调用 std::this_thread::sleep_for
来延迟 10 毫秒。持续时间是一个临时对象 std::chrono::milliseconds(10)
。延迟调用似乎 "normal" 和 "typical" 遵循一些示例代码。然而仔细观察,很明显在每个周期中,临时持续时间对象被创建和销毁一次。
// Loop A.
for (;;)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Do something.
}
现在如果持续时间对象是在循环外创建的(作为常量对象),它将在所有循环中只构造一次。请看下面的代码。
// Loop B.
const auto t = std::chrono::milliseconds(10);
for (;;)
{
std::this_thread::sleep_for(t);
// Do something.
}
问题:由于 std::this_thread::sleep_for 使用 "const &" 作为其参数类型,任何 C++ 编译器都会将循环 A 内的临时持续时间对象优化为类似于循环 B 的对象吗?
我在下面尝试了一个简单的测试程序。结果显示 VC++ 2013 没有优化 "const &" 临时对象。
#include <iostream>
#include <thread>
using namespace std;
class A {
public:
A() { cout << "Ctor.\n"; }
void ReadOnly() const {} // Read-only method.
};
static void Foo(const A & a)
{
a.ReadOnly();
}
int main()
{
cout << "Temp object:\n";
for (int i = 0; i < 3; ++i)
{
Foo(A());
}
cout << "Optimized:\n";
const auto ca = A();
for (int i = 0; i < 3; ++i)
{
Foo(ca);
}
}
/* VC2013 Output:
Temp object:
Ctor.
Ctor.
Ctor.
Optimized:
Ctor.
*/
MSVC 和其他现代编译器完全能够优化循环中的临时对象。
你的例子中的问题是你在构造函数中有一个副作用。根据 C++ 标准,不允许编译器优化临时对象的 creation/destruction,因为它不会再产生相同的可观察效果(即打印 3 次)。
如果您不再 cout
某些东西,情况就完全不同了。当然,您必须查看生成的汇编代码以验证优化。
示例:
class A {
public:
static int k;
A() { k++; }
void ReadOnly() const {} // Read-only method.
};
int A::k = 0;
// Foo unchanged
int main()
{
for(int i = 0; i < 3; ++i)
Foo(A()); // k++ is a side effect, but not yet observable
volatile int x = A::k; // volatile can't be optimized away
const auto ca = A();
for(int i = 0; i < 3; ++i)
Foo(ca);
x = A::k; // volatile can't be optimized away
cout << x << endl;
}
优化器非常清楚地注意到它是递增的同一个静态变量,它没有在其他地方使用。所以这里生成(提取)的汇编代码:
mov eax, DWORD PTR ?k@A@@2HA ; A::k <=== load K
add eax, 3 <=== add 3 to it (NO LOOP !!!)
mov DWORD PTR ?k@A@@2HA, eax ; A::k <=== store k
mov DWORD PTR _x$[ebp], eax <=== store a copy in x
inc eax <=== increment k
<=== (no loop since function doesn't perform anything)
mov DWORD PTR ?k@A@@2HA, eax ; A::k <=== store it
mov DWORD PTR _x$[ebp], eax <=== copy it to x
当然需要在release模式下编译。
如您所见,编译器非常非常聪明。所以让他做他的工作,专注于你的代码设计,并牢记:过早的优化是万恶之源 ;-)
假设编译器 "understands" 构造函数的作用(换句话说,在翻译单元中具有构造函数的源代码 - 即源文件或头文件之一,包含该构造函数的定义),那么编译器应该删除对没有副作用的构造函数的多余调用。
由于打印某些内容是您的 A
构造函数的一个非常明确的副作用,编译器显然无法优化它。所以,编译器在这里做的正是 "right" 事情。如果你有,例如,一个持有锁的构造函数,然后释放析构函数中的锁,并且编译器决定优化你的,那将是非常糟糕的:
for(...)
{
LockWrapper lock_it(theLock);
... some code here
}
到循环之外,因为虽然获取和释放锁的开销较低,但代码的语义发生了变化,锁的持续时间可能会更长,这会对其他代码使用产生影响相同的锁,例如在不同的线程中。
我在下面有一个永远的线程循环调用 std::this_thread::sleep_for
来延迟 10 毫秒。持续时间是一个临时对象 std::chrono::milliseconds(10)
。延迟调用似乎 "normal" 和 "typical" 遵循一些示例代码。然而仔细观察,很明显在每个周期中,临时持续时间对象被创建和销毁一次。
// Loop A.
for (;;)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Do something.
}
现在如果持续时间对象是在循环外创建的(作为常量对象),它将在所有循环中只构造一次。请看下面的代码。
// Loop B.
const auto t = std::chrono::milliseconds(10);
for (;;)
{
std::this_thread::sleep_for(t);
// Do something.
}
问题:由于 std::this_thread::sleep_for 使用 "const &" 作为其参数类型,任何 C++ 编译器都会将循环 A 内的临时持续时间对象优化为类似于循环 B 的对象吗?
我在下面尝试了一个简单的测试程序。结果显示 VC++ 2013 没有优化 "const &" 临时对象。
#include <iostream>
#include <thread>
using namespace std;
class A {
public:
A() { cout << "Ctor.\n"; }
void ReadOnly() const {} // Read-only method.
};
static void Foo(const A & a)
{
a.ReadOnly();
}
int main()
{
cout << "Temp object:\n";
for (int i = 0; i < 3; ++i)
{
Foo(A());
}
cout << "Optimized:\n";
const auto ca = A();
for (int i = 0; i < 3; ++i)
{
Foo(ca);
}
}
/* VC2013 Output:
Temp object:
Ctor.
Ctor.
Ctor.
Optimized:
Ctor.
*/
MSVC 和其他现代编译器完全能够优化循环中的临时对象。
你的例子中的问题是你在构造函数中有一个副作用。根据 C++ 标准,不允许编译器优化临时对象的 creation/destruction,因为它不会再产生相同的可观察效果(即打印 3 次)。
如果您不再 cout
某些东西,情况就完全不同了。当然,您必须查看生成的汇编代码以验证优化。
示例:
class A {
public:
static int k;
A() { k++; }
void ReadOnly() const {} // Read-only method.
};
int A::k = 0;
// Foo unchanged
int main()
{
for(int i = 0; i < 3; ++i)
Foo(A()); // k++ is a side effect, but not yet observable
volatile int x = A::k; // volatile can't be optimized away
const auto ca = A();
for(int i = 0; i < 3; ++i)
Foo(ca);
x = A::k; // volatile can't be optimized away
cout << x << endl;
}
优化器非常清楚地注意到它是递增的同一个静态变量,它没有在其他地方使用。所以这里生成(提取)的汇编代码:
mov eax, DWORD PTR ?k@A@@2HA ; A::k <=== load K
add eax, 3 <=== add 3 to it (NO LOOP !!!)
mov DWORD PTR ?k@A@@2HA, eax ; A::k <=== store k
mov DWORD PTR _x$[ebp], eax <=== store a copy in x
inc eax <=== increment k
<=== (no loop since function doesn't perform anything)
mov DWORD PTR ?k@A@@2HA, eax ; A::k <=== store it
mov DWORD PTR _x$[ebp], eax <=== copy it to x
当然需要在release模式下编译。
如您所见,编译器非常非常聪明。所以让他做他的工作,专注于你的代码设计,并牢记:过早的优化是万恶之源 ;-)
假设编译器 "understands" 构造函数的作用(换句话说,在翻译单元中具有构造函数的源代码 - 即源文件或头文件之一,包含该构造函数的定义),那么编译器应该删除对没有副作用的构造函数的多余调用。
由于打印某些内容是您的 A
构造函数的一个非常明确的副作用,编译器显然无法优化它。所以,编译器在这里做的正是 "right" 事情。如果你有,例如,一个持有锁的构造函数,然后释放析构函数中的锁,并且编译器决定优化你的,那将是非常糟糕的:
for(...)
{
LockWrapper lock_it(theLock);
... some code here
}
到循环之外,因为虽然获取和释放锁的开销较低,但代码的语义发生了变化,锁的持续时间可能会更长,这会对其他代码使用产生影响相同的锁,例如在不同的线程中。