Como as macros prováveis ??/ improvável no trabalho kernel do Linux e qual é o seu benefício?

StackOverflow https://stackoverflow.com/questions/109710

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).

Foi útil?

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top