GCC优化技巧,它真的有用吗?
GCC optimization trick, does it really work?
在查看一些关于优化的问题时,这个关于最有效地使用优化器的编码实践问题的公认答案激起了我的好奇心。断言局部变量应该用于函数中的计算,而不是输出参数。有人建议,这将允许编译器进行额外的优化,否则是不可能的。
因此,为示例Foo类编写一段简单的代码并使用g++ v4.4和-O2编译代码片段会得到一些汇编器输出(使用-S)。仅包含循环部分的汇编程序清单部分如下所示。在检查输出时,似乎两者的循环几乎相同,只是在一个地址上有所不同。该地址是指向第一个示例的输出参数或指向第二个示例的局部变量的指针。
无论是否使用局部变量,实际效果似乎没有变化。所以这个问题可以分为三个部分:
a)是GCC 而不是做额外的优化,即使给出了提示;
b)是GCC 在两种情况下都成功地优化,但不应该;
c) GCC 在两种情况下都成功地优化了吗,并且产生了c++标准定义的兼容输出?
下面是未优化的函数:
void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
for (int i=0; i<numFoo, i++)
{
barOut.munge(foo1, foo2[i]);
}
}
和对应的汇编:
.L3:
movl (%esi), %eax
addl $1, %ebx
addl $4, %esi
movl %eax, 8(%esp)
movl (%edi), %eax
movl %eax, 4(%esp)
movl 20(%ebp), %eax ; Note address is that of the output argument
movl %eax, (%esp)
call _ZN3Foo5mungeES_S_
cmpl %ebx, 16(%ebp)
jg .L3
下面是重写后的函数:
void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
Foo barTemp = barOut;
for (int i=0; i<numFoo, i++)
{
barTemp.munge(foo1, foo2[i]);
}
barOut = barTemp;
}
下面是使用局部变量的函数的编译器输出:
.L3:
movl (%esi), %eax ; Load foo2[i] pointer into EAX
addl $1, %ebx ; increment i
addl $4, %esi ; increment foo2[i] (32-bit system, 8 on 64-bit systems)
movl %eax, 8(%esp) ; PUSH foo2[i] onto stack (careful! from EAX, not ESI)
movl (%edi), %eax ; Load foo1 pointer into EAX
movl %eax, 4(%esp) ; PUSH foo1
leal -28(%ebp), %eax ; Load barTemp pointer into EAX
movl %eax, (%esp) ; PUSH the this pointer for barTemp
call _ZN3Foo5mungeES_S_ ; munge()!
cmpl %ebx, 16(%ebp) ; i < numFoo
jg .L3 ; recall incrementing i by one coming into the loop
; so test if greater
在那个答案中给出的例子不是一个很好的例子,因为调用了一个编译器无法推断的未知函数。下面是一个更好的例子:
void FillOneA(int *array, int length, int& startIndex)
{
for (int i = 0; i < length; i++) array[startIndex + i] = 1;
}
void FillOneB(int *array, int length, int& startIndex)
{
int localIndex = startIndex;
for (int i = 0; i < length; i++) array[localIndex + i] = 1;
}
第一个版本优化得很差,因为它需要防止有人将其称为
int array[10] = { 0 };
FillOneA(array, 5, array[1]);
导致{1, 1, 0, 1, 1, 1, 0, 0, 0, 0 }
,因为与i=1
的迭代修改了startIndex
参数。
第二个不需要担心array[localIndex + i] = 1
会修改localIndex
的可能性,因为localIndex
是一个地址从未被占用的局部变量。
汇编(Intel表示法,因为我用的是Intel表示法):
FillOneA:
mov edx, [esp+8]
xor eax, eax
test edx, edx
jle $b
push esi
mov esi, [esp+16]
push edi
mov edi, [esp+12]
$a: mov ecx, [esi]
add ecx, eax
inc eax
mov [edi+ecx*4], 1
cmp eax, edx
jl $a
pop edi
pop esi
$b: ret
FillOneB:
mov ecx, [esp+8]
mov eax, [esp+12]
mov edx, [eax]
test ecx, ecx
jle $a
mov eax, [esp+4]
push edi
lea edi, [eax+edx*4]
mov eax, 1
rep stosd
pop edi
$a: ret
添加:这里是一个编译器的洞察力是Bar,而不是munge的例子:
class Bar
{
public:
float getValue() const
{
return valueBase * boost;
}
private:
float valueBase;
float boost;
};
class Foo
{
public:
void munge(float adjustment);
};
void Adjust10A(Foo& foo, const Bar& bar)
{
for (int i = 0; i < 10; i++)
foo.munge(bar.getValue());
}
void Adjust10B(Foo& foo, const Bar& bar)
{
Bar localBar = bar;
for (int i = 0; i < 10; i++)
foo.munge(localBar.getValue());
}
结果代码是
Adjust10A:
push ecx
push ebx
mov ebx, [esp+12] ;; foo
push esi
mov esi, [esp+20] ;; bar
push edi
mov edi, 10
$a: fld [esi+4] ;; bar.valueBase
push ecx
fmul [esi] ;; valueBase * boost
mov ecx, ebx
fstp [esp+16]
fld [esp+16]
fstp [esp]
call Foo::munge
dec edi
jne $a
pop edi
pop esi
pop ebx
pop ecx
ret 0
Adjust10B:
sub esp, 8
mov ecx, [esp+16] ;; bar
mov eax, [ecx] ;; bar.valueBase
mov [esp], eax ;; localBar.valueBase
fld [esp] ;; localBar.valueBase
mov eax, [ecx+4] ;; bar.boost
mov [esp+4], eax ;; localBar.boost
fmul [esp+4] ;; localBar.getValue()
push esi
push edi
mov edi, [esp+20] ;; foo
fstp [esp+24]
fld [esp+24] ;; cache localBar.getValue()
mov esi, 10 ;; loop counter
$a: push ecx
mov ecx, edi ;; foo
fstp [esp] ;; use cached value
call Foo::munge
fld [esp]
dec esi
jne $a ;; loop
pop edi
fstp ST(0)
pop esi
add esp, 8
ret 0
注意Adjust10A
的内循环必须重新计算这个值,因为它必须防止foo.munge
改变了bar
的可能性。
也就是说,这种风格的优化并不是一个灌篮。(例如,我们可以通过手动将bar.getValue()
缓存到localValue
来获得相同的效果。)它往往对向量化操作最有帮助,因为这些操作可以并行化。
首先,我要假设munge()
不能内联-也就是说,它的定义不在同一个翻译单元中;你没有提供完整的来源,所以我不能完全确定,但它可以解释这些结果。
由于foo1
作为引用传递给munge
,在实现层,编译器只传递一个指针。如果我们只是转发我们的参数,这是很好的和快速的-任何混叠问题都是munge()
的问题-而且必须是,因为munge()
不能假设任何关于它的参数,我们也不能假设munge()
可能对它们做什么(因为munge()
的定义不可用)。
munge()
可以观察到行为上的差异——如果它接受指向其第一个参数的指针,它可以看到它不等于&foo1
。由于munge()
的实现不在作用域中,编译器不能假设它不会这样做。
这种局部变量复制技巧最终会导致悲观,而不是优化——它试图帮助的优化是不可能的,因为munge()
不能内联;出于同样的原因,局部变量会对性能产生不利影响。
再试一次,确保munge()
不是虚拟的,并且作为一个可内联的函数可用,这将是有指导意义的。
- 在决定是通过参考还是通过价值时,尺寸真的是一个问题吗
- 为什么需要复制构造函数,在哪些情况下它们非常有用
- 其中降频广播实际上是有用的
- 字节真的是最小可寻址单元吗
- 如果我真的真的想从 STL 容器继承,并且我继承构造函数并删除新运算符,会发生什么?
- 如何在 std::vector 中找到<bool>哪些索引是真的?
- std::string 的对象真的可以移动吗?
- 在这种情况下,我真的复制了字节还是复制了字符?
- int8_t和uint8_t真的是整数吗?它们有什么用?
- 真的没有来自 std::string_view 的 std::string 的显式构造函数吗?
- 查找不等式为真的次数时出现问题
- 考虑到其他好处,关键字'auto'真的有助于简化调试C++吗?
- 有没有更好的方法来处理异常? try-catch块真的很丑
- 在为嵌套类定义行外友元时,我真的必须打破封装吗?
- 你如何理解"std: :forward is just syntactic sugar"?这是真的吗?
- 当迭代器(输入参数)通常不是constexpr时,constexpr算法真的有用吗
- C++/CLI - 垃圾收集的效果如何,它真的对我有用吗?
- 引入右值引用真的有用吗
- GCC优化技巧,它真的有用吗?
- 康斯特汽车&&&真的没有用吗?