Percebi pela primeira vez em 2009 que o GCC (pelo menos em meus projetos e em minhas máquinas) tem a tendência de gerar código visivelmente mais rápido se otimizar para tamanho ( -Os
) em vez de velocidade ( -O2
ou -O3
), e fico pensando desde então.
Eu consegui criar código (bastante bobo) que mostra esse comportamento surpreendente e é suficientemente pequeno para ser postado aqui.
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
Se eu compilar com -Os
, são necessários 0,38 s para executar este programa e 0,44 s se for compilado com -O2
ou -O3
. Esses tempos são obtidos de forma consistente e praticamente sem ruído (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Atualização: Mudei todo o código do assembly para o GitHub : Eles deixaram o post inchado e aparentemente agregam muito pouco valor às perguntas, pois os fno-align-*
sinalizadores têm o mesmo efeito.)
Aqui está a montagem gerada com -Os
e -O2
.
Infelizmente, meu entendimento de montagem é muito limitado, então não tenho idéia se o que fiz a seguir foi correto: peguei a montagem -O2
e mesclei todas as suas diferenças na montagem, -Os
exceto pelas .p2align
linhas, resultado aqui . Esse código ainda é executado em 0,38s e a única diferença é o .p2align
material.
Se eu acho corretamente, esses são os preenchimentos para o alinhamento da pilha. De acordo com Por que o GCC pad funciona com os NOPs? isso é feito na esperança de que o código seja executado mais rapidamente, mas aparentemente essa otimização saiu pela culatra no meu caso.
É o preenchimento que é o culpado neste caso? Porquê e como?
O ruído produzido praticamente impossibilita micro otimizações de temporização.
Como garantir que esses alinhamentos acidentais de sorte / azar não interfiram quando faço micro otimizações (não relacionadas ao alinhamento de pilha) no código-fonte C ou C ++?
ATUALIZAR:
Seguindo a resposta de Pascal Cuoq, mexi um pouco nos alinhamentos. Ao passar -O2 -fno-align-functions -fno-align-loops
para o gcc, todos .p2align
saem do assembly e o executável gerado é executado em 0,38s. De acordo com a documentação do gcc :
-Os habilita todas as otimizações de -O2 [mas] -Os desabilita os seguintes sinalizadores de otimização:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Portanto, parece um problema de (des) alinhamento.
Ainda estou cético -march=native
quanto ao sugerido na resposta de Marat Dukhan . Não estou convencido de que não esteja apenas interferindo nesse problema de (des) alinhamento; não tem absolutamente nenhum efeito na minha máquina. (No entanto, votei na resposta dele.)
ATUALIZAÇÃO 2:
Podemos tirar -Os
a foto. Os seguintes tempos são obtidos através da compilação com
-O2 -fno-omit-frame-pointer
0,37s-O2 -fno-align-functions -fno-align-loops
0,37s-S -O2
movendo manualmente a montagemadd()
apóswork()
0,37s-O2
0.44s
Parece-me que a distância do add()
local da chamada é muito importante. Eu tentei perf
, mas a saída perf stat
e perf report
faz muito pouco sentido para mim. No entanto, só consegui obter um resultado consistente:
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Para fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Para -fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Parece que estamos paralisando a chamada add()
no caso lento.
Examinei tudo o que perf -e
pode cuspir na minha máquina; não apenas as estatísticas fornecidas acima.
Para o mesmo executável, o stalled-cycles-frontend
mostra correlação linear com o tempo de execução; Não notei mais nada que se correlacionasse tão claramente. (Comparar stalled-cycles-frontend
para diferentes executáveis não faz sentido para mim.)
Incluí as falhas de cache quando surgiram como o primeiro comentário. Examinei todas as falhas de cache que podem ser medidas na minha máquina perf
, não apenas as fornecidas acima. As falhas de cache são muito barulhentas e mostram pouca ou nenhuma correlação com os tempos de execução.