Você já olhou para a assembléia para ver se há alguma diferença?
Aqui está um pouco de código C.
- int binary(int i){
- i = i + 1;
- return i;
- }
- int unary(int i){
- i++;
- return i;
- }
Aqui está o compilador que estou usando.
- rountree4@cuyahoga ~$ clang --version
- Apple LLVM version 7.0.0 (clang-700.1.76)
- Target: x86_64-apple-darwin15.6.0
- Thread model: posix
Veja como gerar o assembly sem compilá-lo.
- rountree4@cuyahoga ~$ clang -S -O3 foo.c
E aqui está o código de montagem para unary.
- _binary: ## @binary
- .cfi_startproc
- ## BB#0:
- pushq %rbp
- Ltmp0:
- .cfi_def_cfa_offset 16
- Ltmp1:
- .cfi_offset %rbp, -16
- movq %rsp, %rbp
- Ltmp2:
- .cfi_def_cfa_register %rbp
- ## kill: EDI<def> EDI<kill> RDI<def>
- leal 1(%rdi), %eax
- popq %rbp
- retq
- .cfi_endproc
O código gerado é idêntico e, portanto, o desempenho será idêntico (todos os outros parâmetros sendo iguais).
- _unary: ## @unary
- .cfi_startproc
- ## BB#0:
- pushq %rbp
- Ltmp3:
- .cfi_def_cfa_offset 16
- Ltmp4:
- .cfi_offset %rbp, -16
- movq %rsp, %rbp
- Ltmp5:
- .cfi_def_cfa_register %rbp
- ## kill: EDI<def> EDI<kill> RDI<def>
- leal 1(%rdi), %eax
- popq %rbp
- retq
- .cfi_endproc
O código gerado é idêntico e, portanto, o desempenho será idêntico (todos os outros parâmetros sendo iguais).
- Admito que uma seqüência de instruções que requer uma instrução separada para carregar um valor imediato pode ser mais lenta do que uma que não requer uma carga separada. As
lea
instruções acima nãorequerem uma carga separada para o valor imediato. O valor imediato é codificado nas instruções. - Dizendo-me que eu tenho uma regressão de desempenho não é grande coisa. Contar com as pessoas do LLVM que eles têm uma regressão de desempenho é um negócio muito maior. Dizer que eles têm uma regressão de desempenho em um dos caminhos de código mais comuns é algo que eu assumiria apenas com muita cautela. Dizendo-lhes que feriram seu desempenho e não fornecem dados para demonstrar que é uma boa maneira de não ser levado a sério.
Então, como você pode fazer a apresentação de um bug de desempenho com o LLVM?
Primeiro, vejamos se a Intel faz as coisas de maneira diferente.
- rountree@quartz386 ~/Quora$ icc --version
- icc_orig (ICC) 16.0.3 20160415
- Copyright (C) 1985-2016 Intel Corporation. All rights reserved.
Aqui está a assembléia para unário:
- # -- Begin unary
- .text
- # mark_begin;
- .align 16,0x90
- .globl unary
- # --- unary(int)
- unary:
- # parameter 1: %edi
- ..B2.1: # Preds ..B2.0
- .cfi_startproc
- ..___tag_value_unary.4:
- ..L5:
- #6.17
- incl %edi #7.2
- movl %edi, %eax #8.9
- ret #8.9
- .align 16,0x90
- .cfi_endproc
- # LOE
- # mark_end;
- .type unary,@function
- .size unary,.-unary
- .data
- # -- End unary
E aqui está a montagem para o binário, exceto que esta vez estou usando .
n=n+7
- # -- Begin binary
- .text
- # mark_begin;
- .align 16,0x90
- .globl binary
- # --- binary(int)
- binary:
- # parameter 1: %edi
- ..B1.1: # Preds ..B1.0
- .cfi_startproc
- ..___tag_value_binary.1:
- ..L2:
- #1.18
- addl $7, %edi #2.10
- movl %edi, %eax #3.9
- ret #3.9
- .align 16,0x90
- .cfi_endproc
- # LOE
- # mark_end;
- .type binary,@function
- .size binary,.-binary
- .data
- # -- End binary
Talvez, surpreendentemente, usar as instruções
addl
ou incl
requer um adicional movl
para obter o resultado eax
. Esse movimento é dependente do resultado da instrução aritmética anterior; os dois não podem ser executados em paralelo.
Então, o que é mais rápido?
Aqui está o equipamento de teste.
- #include <stdio.h>
- #include <sys/time.h>
- #include <stdint.h>
- int binary(int i){
- i = i + 7;
- return i;
- }
- int unary(int i){
- i++;
- return i;
- }
- int a;
- volatile uint64_t i, j;
- int main(){
- struct timeval start, end;
- gettimeofday(&start, NULL);
- for(i = 0; i<100; i++){
- for (j=0; j<1000000; j++){
- a = binary(a);
- }
- }
- gettimeofday(&end, NULL);
- fprintf(stdout, "Binary time in seconds: %lf\n",
- (end.tv_sec - start.tv_sec) +
- (end.tv_usec - start.tv_usec)/1000000.0);
- gettimeofday(&start, NULL);
- for(i = 0; i<100; i++){
- for (j=0; j<1000000; j++){
- a = unary(a);
- }
- }
- gettimeofday(&end, NULL);
- fprintf(stdout, "Unary time in seconds: %lf\n",
- (end.tv_sec - start.tv_sec) +
- (end.tv_usec - start.tv_usec)/1000000.0);
- gettimeofday(&start, NULL);
- for(i = 0; i<100; i++){
- for (j=0; j<1000000; j++){
- }
- }
- gettimeofday(&end, NULL);
- fprintf(stdout, "Loop time in seconds: %lf\n",
- (end.tv_sec - start.tv_sec) +
- (end.tv_usec - start.tv_usec)/1000000.0);
- return !a;
- }
Aqui estão os resultados para o compilador Intel icc.
- rountree@quartz386 ~/Quora$ ./icc.out
- Binary time in seconds: 0.270814
- Unary time in seconds: 0.270599
- Loop time in seconds: 0.231791
E aqui estão os resultados do gcc 4.9.3, pois não tenho clang e icc na mesma máquina, mas gcc também usa código idêntico baseado em lea para ambas as funções.
- rountree@quartz386 ~/Quora$ ./gcc.out
- Binary time in seconds: 0.232028
- Unary time in seconds: 0.231783
- Loop time in seconds: 0.231703
O tempo de loop está próximo o suficiente para idêntico. Em loop + tempo de função, porém, o
lea
código é significativamente mais rápido. Quanto?
- Function time (seconds)
- binary unary
- gcc 0.000245 0.000080
- intel 0.039023 0.038808
Nessa instância particular, reivindicar
inc
foi mais rápido do que lea
errado por duas ordens de grandeza.
Estou ansioso para dar a Intel um tempo difícil sobre isso quando eu estiver visitando eles em Hillsboro na próxima semana.
0 Comentários