Qual código é melhor para a otimização de previsão de ramificação?

10

Dada a previsão de ramificação e também o efeito das otimizações do compilador, qual código tende a oferecer desempenho superior?

Observe que bRareExceptionPresent representa uma condição incomum. Não é o caminho normal da lógica.

/* MOST COMMON path must branch around IF clause */

bool SomeFunction(bool bRareExceptionPresent)
{
  // abort before function
  if(bRareExceptionPresent)
  {
     return false;
  }    
  .. function primary body ..    
  return true;
}

/* MOST COMMON path does NOT branch */

bool SomeFunction(bool bRareExceptionPresent)
{
  if(!bRareExceptionPresent)
  {
    .. function primary body ..
  }
  else
  {
    return false;
  }
  return true;
}
dyasta
fonte
9
Vou sair do ramo aqui e dizer que não há diferença alguma.
Robert Harvey
7
Provavelmente, isso depende da CPU específica que você está compilando, pois elas têm arquiteturas de pipelining diferentes (slots de atraso versus nenhum slot de atraso). O tempo que você gastou pensando nisso provavelmente é muito mais do que o tempo economizado ao executar - primeiro o perfil e, em seguida, otimize.
2
É quase certamente micro-otimização prematura.
21713 Robert Harvey
2
@MichaelT Sim, a criação de perfil é realmente a única maneira confiável de saber o que realmente está acontecendo com o desempenho do código no destino, plataforma, dentro de seu contexto. No entanto, eu estava curioso para saber se um era o preferido.
precisa saber é o seguinte
11
@RobertHarvey: É uma micro-otimização prematura, exceto nos casos em que as duas condições são atendidas: (1) o loop é chamado bilhões (não milhões) de vezes; e (2) ironicamente, quando o corpo do loop é pequeno em termos de código de máquina. A condição 2 significa que a fração do tempo gasto em despesas gerais não é insignificante em comparação com o tempo gasto em trabalho útil. A boa notícia é que, geralmente, em situações em que as duas condições são atendidas, o SIMD (vetorização), que por natureza é sem ramo, resolverá todos os problemas de desempenho.
rwong

Respostas:

10

No mundo de hoje, não importa muito, se é que realmente.

A previsão dinâmica de ramificação (algo pensado por décadas (consulte Uma Análise dos Esquemas Dinâmicos de Predição de Ramificação em Cargas de Trabalho do Sistema publicadas em 1996)) é um lugar bastante comum.

Um exemplo disso pode ser encontrado no processador ARM. Do Centro de Informações do Braço na Previsão de Filiais

Para melhorar a precisão da previsão de ramificação, é empregada uma combinação de técnicas estáticas e dinâmicas.

A questão então é "o que é predição dinâmica de ramificação no processador de braço?" A leitura contínua da previsão de ramificação dinâmica mostra que ele usa um esquema de previsão de 2 bits (descrito no artigo) cria informações sobre se a ramificação é forte ou fracamente captada ou não.

Com o tempo (e com o tempo, quero dizer algumas passagens por esse bloco), isso cria informações sobre como o código irá seguir.

Para previsão estática , ele analisa a aparência do código e de que maneira a ramificação é feita no teste - para uma instrução anterior ou outra no código:

O esquema usado no processador ARM1136JF-S prevê que nem todas as ramificações condicionais para frente são tomadas e todas as ramificações para trás são tomadas. Cerca de 65% de todas as ramificações são precedidas por ciclos não ramificados suficientes para serem completamente previstos.

Como mencionado por Sparky, isso se baseia no entendimento de que o loop é repetido com mais freqüência do que não. O loop se ramifica para trás (ele possui uma ramificação no final do loop para reiniciá-lo na parte superior) - normalmente faz isso.

O perigo de tentar adivinhar o compilador é que você não sabe como esse código será realmente compilado (e otimizado). E na maioria das vezes, isso não importa. Com a previsão dinâmica, duas vezes por meio da função, ele prevê uma pular a declaração de guarda para um retorno prematuro. Se o desempenho de dois pipelines liberados for de desempenho crítico, há outras coisas com que se preocupar.

O tempo que leva para ler um estilo sobre o outro é provavelmente de maior importância - tornando o código limpo para que um humano possa lê-lo, porque o compilador vai se sair bem, não importa o quão confuso ou idealizado você escreva o código.


fonte
7
Uma famosa questão de stackoverflow mostrou que a previsão de ramificação é importante até hoje.
Florian Margaine
3
@FlorianMargaine, embora isso importe, ele entra em uma situação em que realmente importa parece exigir a compreensão do que você está compilando e como ele funciona (braço x x86 x mips ...). Escrever código tentando fazer essa micro-otimização no início provavelmente está funcionando a partir de premissas equivocadas e não atinge o efeito desejado.
Bem, é claro, não vamos citar DK. Mas acho que essa pergunta foi claramente no sentido de otimização, quando você já passou da fase de criação de perfil. :-)
Florian Margaine
2
@ MichaelT Nice resposta, e eu concordo muito com a sua conclusão. Esse tipo de pré-criação de perfil / otimização abstrata pode definitivamente ser contraproducente. Ele acaba sendo um jogo de adivinhação, fazendo com que alguém tome decisões de design por razões irracionais. Ainda assim, fiquei curioso; o
dyasta 28/04
5
@ 90h stackoverflow.com/questions/11227809/…
Florian Margaine
9

Meu entendimento é que, na primeira vez em que a CPU encontrar uma ramificação, ela preverá (se houver suporte) que ramificações para frente não são obtidas e ramificações para trás. A justificativa para isso é que os loops (que geralmente se ramificam para trás) são assumidos.

Em alguns processadores, você pode dar uma dica nas instruções de montagem sobre qual caminho é o mais provável. Detalhes disso me escapam no momento.

Além disso, alguns compiladores C também oferecem suporte à previsão de ramificação estática, para que você possa dizer ao compilador qual ramificação é mais provável. Por sua vez, pode reorganizar o código gerado ou usar instruções modificadas para tirar proveito dessas informações (ou até mesmo ignorá-las).

__builtin_expect((long)!!(x), 1L)  /* GNU C to indicate that <x> will likely be TRUE */
__builtin_expect((long)!!(x), 0L)  /* GNU C to indicate that <x> will likely be FALSE */

Espero que isto ajude.

Sparky
fonte
3
"Meu entendimento é que, na primeira vez em que a CPU encontrar uma ramificação, ela preverá (se houver suporte) que ramificações para frente não são obtidas e ramificações para trás". Este é um pensamento muito interessante. Você tem alguma evidência de que isso seja realmente implementado em arquiteturas comuns?
Blubb
5
Diretamente da boca do cavalo: Um ramo para a frente é padrão para não ser levado. O desvio para trás é assumido como padrão . E a partir da mesma página: "prefixo 0x3E - prever estaticamente uma ramificação como tomada".
precisa saber é o seguinte
Existe um pragma agnóstico de plataforma que seja equivalente a __builtin_expect?
MarcusJ