Eu estive pesquisando algumas partes do kernel do Linux e encontrei chamadas como esta:
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 otimização, mas como eles funcionam? E quanta redução de desempenho / tamanho pode ser esperada ao usá-los? E vale a pena o aborrecimento (e provavelmente a perda da portabilidade) pelo menos no código de gargalo (no espaço do usuário, é claro).
linux
gcc
linux-kernel
likely-unlikely
terminus
fonte
fonte
BOOST_LIKELY
__builtin_expect
em outra questão.#define likely(x) (x)
e#define unlikely(x) (x)
em plataformas que não suportam esse tipo de dica.Respostas:
Eles sugerem que o compilador emita instruções que farão com que a predição de ramificação seja favorável ao lado "provável" de uma instrução de salto. Isso pode ser uma grande vitória, se a previsão estiver correta, significa que a instrução de salto é basicamente livre e levará zero ciclos. Por outro lado, se a previsão estiver incorreta, significa que o pipeline do processador precisa ser liberado e pode custar vários ciclos. Enquanto a previsão estiver correta na maioria das vezes, isso tenderá a ser bom para o desempenho.
Como todas essas otimizações de desempenho, você deve fazê-lo apenas após uma extensa criação de perfil para garantir que o código esteja realmente em um gargalo e, provavelmente, dada a natureza micro, que ele esteja sendo executado em um loop restrito. Geralmente os desenvolvedores do Linux são bem experientes, então eu imagino que eles teriam feito isso. Eles realmente não se importam muito com a portabilidade, pois só têm como alvo o gcc e têm uma ideia muito próxima do assembly que desejam que ele gere.
fonte
"[...]that it is being run in a tight loop"
, muitas CPUs possuem um preditor de ramificação , portanto, o uso dessas macros ajuda apenas na primeira execução do código ou quando a tabela de histórico é substituída por uma ramificação diferente com o mesmo índice na tabela de ramificação. Em um loop restrito, e supondo que uma ramificação seja uma maneira na maioria das vezes, o preditor de ramificação provavelmente começará a adivinhar a ramificação correta muito rapidamente. - seu amigo de pediatria.Vamos descompilar para ver o que o GCC 4.8 faz com ele
Sem
__builtin_expect
Compile e descompile com o GCC 4.8.2 x86_64 Linux:
Resultado:
A ordem das instruções na memória permaneceu inalterada: primeiro o
printf
e depoisputs
oretq
retorno.Com
__builtin_expect
Agora substitua
if (i)
por:e temos:
O
printf
(compilado para__printf_chk
) foi movido para o final da função, apósputs
e o retorno para melhorar a previsão de ramificação, conforme mencionado por outras respostas.Portanto, é basicamente o mesmo que:
Essa otimização não foi concluída
-O0
.Mas boa sorte em escrever um exemplo que seja mais rápido do
__builtin_expect
que sem, as CPUs são realmente inteligentes atualmente . Minhas tentativas ingênuas estão aqui .C ++ 20
[[likely]]
e[[unlikely]]
O C ++ 20 padronizou os recursos internos do C ++: Como usar o atributo provável / improvável do C ++ 20 na instrução if-else Eles provavelmente (um trocadilho!) Farão a mesma coisa.
fonte
Essas são macros que dão dicas ao compilador sobre o caminho a ser seguido por uma ramificação. As macros se expandem para extensões específicas do GCC, se estiverem disponíveis.
O GCC usa esses recursos para otimizar a previsão de ramificação. Por exemplo, se você tiver algo parecido com o seguinte
Em seguida, ele pode reestruturar esse código para algo mais ou menos como:
O benefício disso é que, quando o processador entra em uma ramificação pela primeira vez, há uma sobrecarga significativa, porque pode estar carregando e executando especulativamente o código mais adiante. Quando determina que a ramificação será executada, ela deve ser invalidada e iniciada no destino da ramificação.
Os processadores mais modernos agora têm algum tipo de previsão de ramificação, mas isso só ajuda quando você já passou pela ramificação antes, e a ramificação ainda está no cache de previsão de ramificação.
Existem várias outras estratégias que o compilador e o processador podem usar nesses cenários. Você pode encontrar mais detalhes sobre como os preditores de agência funcionam na Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor
fonte
goto
s sem repetir oreturn x
: stackoverflow.com/a/31133787/895245Eles fazem com que o compilador emita as dicas de ramificação apropriadas onde o hardware as suporta. Isso geralmente significa apenas girar alguns bits no opcode da instrução, para que o tamanho do código não seja alterado. A CPU começará a buscar instruções no local previsto e liberará o pipeline e reiniciará se isso estiver errado quando a ramificação for alcançada; no caso em que a dica estiver correta, isso tornará a ramificação muito mais rápida - precisamente quanto mais rapidamente dependerá do hardware; e quanto isso afeta o desempenho do código dependerá de qual proporção de tempo a dica está correta.
Por exemplo, em uma CPU do PowerPC, uma ramificação não sugerida pode levar 16 ciclos, uma incorreta com 8 e outra incorretamente com 24. Nos loops mais internos, uma boa sugestão pode fazer uma enorme diferença.
Portabilidade não é realmente um problema - presumivelmente a definição está em um cabeçalho por plataforma; você pode simplesmente definir "provável" e "improvável" para plataformas que não suportam dicas de ramificação estática.
fonte
Essa construção informa ao compilador que a expressão EXP provavelmente terá o valor C. O valor de retorno é EXP. __builtin_expect deve ser usado em uma expressão condicional. Em quase todos os casos, será usado no contexto de expressões booleanas; nesse caso, é muito mais conveniente definir duas macros auxiliares:
Essas macros podem então ser usadas como em
Referência: https://www.akkadia.org/drepper/cpumemory.pdf
fonte
__builtin_expect(!!(expr),0)
em vez de apenas__builtin_expect((expr),0)
?!!
é equivalente a converter algo para abool
. Algumas pessoas gostam de escrever dessa maneira.(comentário geral - outras respostas cobrem os detalhes)
Não há motivo para perder a portabilidade usando-os.
Você sempre tem a opção de criar uma macro "inline" ou efeito nulo simples que permita compilar em outras plataformas com outros compiladores.
Você simplesmente não terá o benefício da otimização se estiver em outras plataformas.
fonte
De acordo com o comentário de Cody , isso não tem nada a ver com o Linux, mas é uma dica para o compilador. O que acontece dependerá da arquitetura e da versão do compilador.
Esse recurso específico no Linux é um pouco mal utilizado nos drivers. Como o osgx aponta na semântica do atributo hot , qualquer função
hot
oucold
função chamada em um bloco pode sugerir automaticamente que a condição é provável ou não. Por exemplo,dump_stack()
está marcado,cold
então isso é redundante,Versões futuras de
gcc
podem seletivamente alinhar uma função com base nessas dicas. Também houve sugestões de que nãoboolean
, mas uma pontuação como a mais provável etc. Geralmente, deve ser preferido usar algum mecanismo alternativo comocold
. Não há razão para usá-lo em qualquer lugar, exceto em caminhos quentes. O que um compilador fará em uma arquitetura pode ser completamente diferente em outra.fonte
Em muitas versões do linux, você pode encontrar complier.h em / usr / linux /, incluindo-o para uso simples. E outra opinião, improvável () é mais útil do que provável (), porque
Ele também pode ser otimizado em muitos compiladores.
A propósito, se você quiser observar o comportamento detalhado do código, faça o seguinte:
Em seguida, abra obj.s, você pode encontrar a resposta.
fonte
Eles são dicas para o compilador para gerar os prefixos de dicas nas ramificações. No x86 / x64, eles ocupam um byte, então você terá no máximo um aumento de um byte para cada ramificação. Quanto ao desempenho, depende inteiramente do aplicativo - na maioria dos casos, o preditor de ramificação do processador os ignorará atualmente.
Edit: Esqueceu-se de um lugar que eles realmente podem realmente ajudar. Ele pode permitir que o compilador reordene o gráfico de fluxo de controle para reduzir o número de ramificações realizadas para o caminho 'provável'. Isso pode ter uma melhoria acentuada nos loops onde você está verificando vários casos de saída.
fonte
Essas são funções do GCC para o programador dar uma dica ao compilador sobre qual será a condição de ramificação mais provável em uma determinada expressão. Isso permite que o compilador construa as instruções de ramificação para que o caso mais comum leve o menor número de instruções para executar.
Como as instruções de ramificação são construídas depende da arquitetura do processador.
fonte