Como as macros prováveis ??/ improvável no trabalho kernel do Linux e qual é o seu benefício?
-
02-07-2019 - |
Pergunta
Eu estive cavando através de algumas partes do kernel Linux, e encontrados chamadas como este:
if (unlikely(fd < 0))
{
/* Do something */
}
ou
if (likely(!err))
{
/* Do something */
}
Eu encontrei a definição deles:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
Eu sei que eles são para a otimização, mas como eles funcionam? E quanto diminuição performance / tamanho pode ser esperado de usá-los? E vale a pena o incómodo (e perder a portabilidade provavelmente) pelo menos em código gargalo (no espaço do usuário, é claro).
Solução
Eles são dica para o compilador para emitir instruções que fará com previsão de desvios para favorecer o lado "provável" de uma instrução de salto. Esta pode ser uma grande vitória, se a previsão está correta, significa que a instrução de salto é basicamente livre e terá zero ciclos. Por outro lado, se a previsão é errado, então isso significa que o gasoduto processador precisa ser lavada e pode custar vários ciclos. Enquanto a previsão está correta na maioria das vezes, esta tenderá a ser bom para o desempenho.
Como todas essas otimizações de desempenho que você só deve fazê-lo após extensa de perfis para garantir o código realmente está em um gargalo, e, provavelmente, dada a micro natureza, que ele está sendo executado em um loop apertado. Geralmente os desenvolvedores Linux são muito experientes, então eu imagino que eles teriam feito isso. Eles realmente não se importam muito sobre a portabilidade como eles só alvo gcc, e eles têm uma ideia muito perto do conjunto que eles querem gerar.
Outras dicas
Estes são macros que dão dicas para o compilador sobre qual caminho a sucursal pode ir. As macros expandir para extensões específicas do CCG, se eles estiverem disponíveis.
GCC utiliza para otimizar para previsão de desvios. Por exemplo, se você tem algo parecido com o seguinte
if (unlikely(x)) {
dosomething();
}
return x;
Em seguida, ele pode reestruturar este código para ser algo mais parecido com:
if (!x) {
return x;
}
dosomething();
return x;
O benefício disso é que quando o processador leva um ramo pela primeira vez, há uma sobrecarga significativa, porque ele pode ter sido especulativamente carregar e executar código mais adiante. Quando se determina que levará o ramo, então ele tem que invalida que, e começar no alvo ramo.
A maioria dos processadores modernos agora têm algum tipo de previsão de desvios, mas isso só ajuda quando você passou o ramo antes, eo ramo ainda está no cache previsão de desvios.
Há uma série de outras estratégias que o compilador eo processador pode usar nestes cenários. Você pode encontrar mais detalhes sobre como ramo preditores trabalho na Wikipedia: http://en.wikipedia.org/wiki / Branch_predictor
Vamos descompilar para ver o GCC 4.8 faz com ele
Sem __builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Use time to prevent it from being optimized away. */
int i = !time(NULL);
if (i)
printf("%d\n", i);
puts("a");
return 0;
}
Compilar e descompilar com GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Output:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 75 14 jne 24 <main+0x24>
10: ba 01 00 00 00 mov $0x1,%edx
15: be 00 00 00 00 mov $0x0,%esi
16: R_X86_64_32 .rodata.str1.1
1a: bf 01 00 00 00 mov $0x1,%edi
1f: e8 00 00 00 00 callq 24 <main+0x24>
20: R_X86_64_PC32 __printf_chk-0x4
24: bf 00 00 00 00 mov $0x0,%edi
25: R_X86_64_32 .rodata.str1.1+0x4
29: e8 00 00 00 00 callq 2e <main+0x2e>
2a: R_X86_64_PC32 puts-0x4
2e: 31 c0 xor %eax,%eax
30: 48 83 c4 08 add $0x8,%rsp
34: c3 retq
A ordem de instrução na memória manteve-se inalterada: em primeiro lugar o printf
e depois puts
eo retorno retq
Com __builtin_expect
Agora substitua if (i)
com:
if (__builtin_expect(i, 0))
e obtém-se:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 74 11 je 21 <main+0x21>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1+0x4
15: e8 00 00 00 00 callq 1a <main+0x1a>
16: R_X86_64_PC32 puts-0x4
1a: 31 c0 xor %eax,%eax
1c: 48 83 c4 08 add $0x8,%rsp
20: c3 retq
21: ba 01 00 00 00 mov $0x1,%edx
26: be 00 00 00 00 mov $0x0,%esi
27: R_X86_64_32 .rodata.str1.1
2b: bf 01 00 00 00 mov $0x1,%edi
30: e8 00 00 00 00 callq 35 <main+0x35>
31: R_X86_64_PC32 __printf_chk-0x4
35: eb d9 jmp 10 <main+0x10>
O printf
(compilado para __printf_chk
) foi transferida para o fim da função, após puts
e o retorno para melhorar a previsão ramo como mencionado por outras respostas.
Por isso, é basicamente o mesmo que:
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
Esta otimização não foi feito com -O0
.
Mas boa sorte em escrever um exemplo que corre mais rápido com __builtin_expect
do que sem, são realmente inteligentes aqueles dias . Minhas tentativas ingênuas está aqui .
Eles causar o compilador para emitir as dicas ramo apropriado, onde os suportes de hardware eles. Isso geralmente significa apenas girando alguns bits no opcode da instrução, por isso o tamanho do código não vai mudar. A CPU vai começar a buscar instruções a partir do local previsto, e lavar as redes e começar de novo se que acaba por estar errado quando o ramo é atingido; no caso em que a dica é correta, isso fará com que o ramo muito mais rápido - precisamente o quanto mais rápido vai depender do hardware; e quanto isso afeta o desempenho do código vai depender do que proporção da dica tempo está correto.
Por exemplo, em um PowerPC CPU um ramo unhinted pode demorar 16 ciclos, um corretamente sugeriu um 8 e um sugerido incorretamente um 24. Em mais interna laços boa insinuando pode fazer uma enorme diferença.
Portabilidade não é realmente um problema - provavelmente a definição está em um cabeçalho per-plataforma; você pode simplesmente definir "provável" e "improvável" a nada para plataformas que não suportam dicas de filiais estáticos.
long __builtin_expect(long EXP, long C);
Esta construção informa ao compilador que o EXP expressão provavelmente terá o valor C. O valor de retorno é EXP. __ builtin_expect é feito para ser usado em uma condicional expressão. Em quase todos os casos, serão utilizados na contexto das expressões booleanas caso em que ele é muito mais conveniente para definir duas macros auxiliares:
#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)
Estas macros podem então ser usadas como em
if (likely(a > 1))
Referência: https://www.akkadia.org/drepper/cpumemory.pdf
(comentário geral - outras respostas cobrir os detalhes)
Não há nenhuma razão que você deve perder a portabilidade, utilizando-os.
Você sempre tem a opção de criar um nulo efeito simples "inline" ou macro que permitirá que você para compilar em outras plataformas com outros compiladores.
Você só não vai obter o benefício da otimização se você estiver em outras plataformas.
De acordo com o comentário por Cody , isso não tem nada a ver com Linux, mas é uma dica para o compilador. O que acontece vai depender da versão de arquitetura e compilador.
Esta característica particular em Linux é um pouco mis-utilizados nos drivers. Como osgx aponta em semântica do atributo quente, qualquer função hot
ou cold
chamado com em um bloco pode sugerir automaticamente que a condição é provável ou não. Por exemplo, dump_stack()
é marcado cold
por isso esta é redundante,
if(unlikely(err)) {
printk("Driver error found. %d\n", err);
dump_stack();
}
As futuras versões do gcc
pode seletivamente em linha uma função com base nessas dicas. Houve também sugestões de que não é boolean
, mas a pontuação como em mais provável , etc. Geralmente, deve ser preferido usar algum mecanismo alternativo como cold
. Não há nenhuma razão para usá-lo em qualquer lugar, mas os caminhos quentes. O que um compilador vai fazer em uma arquitetura pode ser completamente diferente em outra.
Em muitos liberação linux, você pode encontrar complier.h em / usr / linux /, você pode incluí-lo para uso simplesmente. E outra opinião, pouco provável () é mais útil ao invés de probabilidade (), porque
if ( likely( ... ) ) {
doSomething();
}
pode ser otimizado, bem como em muitos compilador.
E, a propósito, se você quiser observar o comportamento do código de detalhe, você pode fazer simplesmente a seguinte:
gcc -c test.c objdump -d test.o> obj.s
Então, obj.s abertos, você pode encontrar a resposta.
Eles são dicas para o compilador para gerar os prefixos dica sobre ramos. Em x86 / x64, eles ocupam um byte, então você vai ter, no máximo, um aumento de um byte para cada ramo. Quanto ao desempenho, que depende inteiramente da aplicação -. Na maioria dos casos, o previsor de ramos sobre o processador irá ignorá-los, estes dias
Editar: Esqueceu-se cerca de um lugar que eles realmente podem realmente ajudar com. Ele pode permitir que o compilador para reordenar o gráfico de controle de fluxo para reduzir o número de agências tomadas para o caminho 'provável'. Isso pode ter uma melhoria marcada em loops onde você está verificando vários casos de saída.
Estas são funções do CCG para o programador para dar uma dica para o compilador sobre o que a condição ramo provavelmente será em uma determinada expressão. Isso permite que o compilador para construir as instruções de desvio para que o caso mais comum leva o menor número de instruções para executar.
Como as instruções de desvio são construídos são dependentes da arquitetura do processador.