GCC优化技巧,真的有用吗?
Posted
技术标签:
【中文标题】GCC优化技巧,真的有用吗?【英文标题】:GCC optimization trick, does it really work? 【发布时间】:2011-10-14 00:03:50 【问题描述】:在查看一些关于优化的问题时,accepted answer 中关于最有效地使用优化器的编码实践的问题激起了我的好奇心。断言是局部变量应该用于函数中的计算,而不是输出参数。有人建议这将允许编译器进行额外的优化,否则是不可能的。
因此,为示例 Foo 类编写一段简单的代码并使用 g++ v4.4 和 -O2 编译代码片段会得到一些汇编程序输出(使用 -S)。汇编程序清单的部分仅包含如下所示的循环部分。在检查输出时,似乎两者的循环几乎相同,只有一个地址不同。该地址是指向第一个示例的输出参数或第二个示例的局部变量的指针。
无论是否使用局部变量,实际效果似乎都没有变化。所以问题分解为 3 个部分:
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
【问题讨论】:
链接到的问题是社区维基,所以如果有人也想做这个,请这样做。我看不出有什么办法。 我们这里说的源码是什么?您能否发布代码并标记代码的哪一部分与您发布的汇编程序摘录相对应? 我已将链接问题中的代码移至此问题中,但如果您使用不同的代码,请替换它。它看起来大致相同,但细节决定一切。 IMO,通过只使用一个编译器和一个平台,你正在徘徊在坏习惯的“黑暗面”。您只提到 GCC,但还有其他编译器,并且有不同的 cpu。如果它们的行为方式与 GCC 不同,那么您的“优化”将浪费时间。我建议坚持使用 C++ 标准并仅在情况需要时执行像这样的低级“优化”(使用分析器检查),但即使那样你也可以使用汇编来强制优化你想要而不是试图预测优化器将对您的代码做什么。 @samoid,代码相同。 【参考方案1】:该答案中给出的示例不是一个很好的示例,因为调用了一个编译器无法推理的未知函数。这是一个更好的例子:
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
是一个地址从未被占用过的局部变量。
在汇编中(英特尔表示法,因为这是我使用的):
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
中来获得相同的效果。)它往往对矢量化操作最有帮助,因为它们可以并行化。
【讨论】:
你的例子我得到了类似的结果。看来,要使这种优化技巧起作用,编译器必须能够清楚地访问所有正在优化的代码。总之,我的问题的答案是 a),GCC 没有使用优化,并且可能不会使用。 如果缓存到局部变量中的对象适合寄存器,您更有可能看到优化产生的效果。【参考方案2】:首先,我假设munge()
不能被内联——也就是说,它的定义不在同一个翻译单元中;你没有提供完整的来源,所以我不能完全确定,但它会解释这些结果。
由于foo1
作为引用传递给munge
,所以在实现级别,编译器只传递一个指针。如果我们只是转发我们的论点,这很好而且很快——任何别名问题都是munge()
的问题——而且必须如此,因为munge()
不能假设任何关于它的论点,我们也不能假设任何关于munge()
可能对它们做什么(因为 munge()
的定义不可用)。
但是,如果我们复制到一个局部变量,我们必须复制到一个局部变量并传递一个指向该局部变量的指针。这是因为munge()
可以观察到行为上的差异——如果它使用指向其第一个参数的指针,它可以看到它不等于&foo1
。由于munge()
的实现不在作用域内,编译器不能假设它不会这样做。
因此,这里的这种局部变量复制技巧最终是悲观的,而不是优化 - 它试图帮助的优化是不可能的,因为 munge()
不能被内联;出于同样的原因,局部变量会严重影响性能。
再试一次会很有启发性,确保munge()
是非虚拟的并且可用作内联函数。
【讨论】:
我还要说,一个可以内联的函数和一个必须将 this 指针传递到的函数(通常是通过堆栈)之间存在一个主要区别。 @inflagranti,即使它不能被内联(例如,由于太大),如果定义可用,编译器可能能够根据其行为进行优化 我的 Foo 实现相当简单,因此优化器在内联时可以使用它。但是,很明显,对于我拥有的代码,优化的代码在 munge() 内联时将结果变量的加载和存储保存在循环中。因此,如果编译器必须调用一个函数而不能内联它,那么给出的优化就没有任何用处。以上是关于GCC优化技巧,真的有用吗?的主要内容,如果未能解决你的问题,请参考以下文章
15 个有用的 MySQL/MariaDB 性能调整和优化技巧
15 个有用的 MySQL/MariaDB 性能调整和优化技巧