Suponha que eu tenha várias instruções que desejo executar em uma ordem fixa. Quero usar o g ++ com o nível de otimização 2, portanto, algumas instruções podem ser reordenadas. Quais ferramentas são necessárias para impor uma certa ordem de declarações?
Considere o seguinte exemplo.
using Clock = std::chrono::high_resolution_clock;
auto t1 = Clock::now(); // Statement 1
foo(); // Statement 2
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
Neste exemplo, é importante que as instruções 1-3 sejam executadas na ordem fornecida. No entanto, o compilador não pode pensar que a instrução 2 é independente de 1 e 3 e executar o código da seguinte maneira?
using Clock=std::chrono::high_resolution_clock;
foo(); // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
c++
c++11
operator-precedence
S2108887
fonte
fonte
__sync_synchronize()
ser de alguma ajuda?foo
de execução, que o compilador pode ignorar ao reordenar, assim como pode ignorar a observação de um thread diferente.Respostas:
Eu gostaria de tentar fornecer uma resposta um pouco mais abrangente depois que isso foi discutido com o comitê de padrões C ++. Além de ser membro do comitê C ++, também sou desenvolvedor dos compiladores LLVM e Clang.
Fundamentalmente, não há como usar uma barreira ou alguma operação na sequência para realizar essas transformações. O problema fundamental é que a semântica operacional de algo como uma adição de inteiro é totalmente conhecida pela implementação. Pode simulá-los, sabe que não podem ser observados por programas corretos e está sempre livre para movê-los.
Poderíamos tentar evitar isso, mas teria resultados extremamente negativos e, no final das contas, falharia.
Primeiro, a única maneira de evitar isso no compilador é dizer a ele que todas essas operações básicas são observáveis. O problema é que isso impediria a grande maioria das otimizações do compilador. Dentro do compilador, essencialmente não temos bons mecanismos para modelar que o tempo é observável, mas nada mais. Não temos nem um bom modelo de quais operações demoram . Por exemplo, a conversão de um inteiro não assinado de 32 bits em um inteiro não assinado de 64 bits leva tempo? Leva tempo zero em x86-64, mas em outras arquiteturas leva tempo diferente de zero. Não há uma resposta genericamente correta aqui.
Mas mesmo se tivermos sucesso por meio de atos heroicos em impedir que o compilador reordene essas operações, não há garantia de que isso será suficiente. Considere uma forma válida e conforme para executar seu programa C ++ em uma máquina x86: DynamoRIO. Este é um sistema que avalia de forma dinâmica o código de máquina do programa. Uma coisa que ele pode fazer são otimizações online e é até capaz de executar especulativamente toda a gama de instruções aritméticas básicas fora do tempo. E esse comportamento não é exclusivo dos avaliadores dinâmicos, a CPU x86 real também especulará (um número muito menor de) instruções e as reordenará dinamicamente.
A compreensão essencial é que o fato de a aritmética não ser observável (mesmo no nível do tempo) é algo que permeia as camadas do computador. É verdade para o compilador, o tempo de execução e, muitas vezes, até mesmo para o hardware. Forçar que seja observável restringiria dramaticamente o compilador, mas também restringiria drasticamente o hardware.
Mas tudo isso não deve fazer você perder a esperança. Quando você deseja cronometrar a execução de operações matemáticas básicas, temos técnicas bem estudadas que funcionam de forma confiável. Normalmente, eles são usados ao fazer micro-benchmarking . Dei uma palestra sobre isso na CppCon2015: https://youtu.be/nXaxk27zwlk
As técnicas mostradas também são fornecidas por várias bibliotecas de micro-benchmarks, como a do Google: https://github.com/google/benchmark#preventing-optimization
A chave para essas técnicas é focar nos dados. Você torna a entrada para o cálculo opaca para o otimizador e o resultado do cálculo opaco para o otimizador. Depois de fazer isso, você pode cronometrar de forma confiável. Vejamos uma versão realista do exemplo na questão original, mas com a definição de
foo
totalmente visível para a implementação. Também extraí uma versão (não portátil) daDoNotOptimize
biblioteca do Google Benchmark, que você pode encontrar aqui: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208Aqui, garantimos que os dados de entrada e os dados de saída sejam marcados como não otimizáveis em torno do cálculo
foo
, e apenas em torno desses marcadores os tempos são calculados. Como você está usando dados para pinçar o cálculo, é garantido que ele permaneça entre os dois tempos e, ainda assim, o cálculo em si pode ser otimizado. O conjunto x86-64 resultante gerado por uma compilação recente do Clang / LLVM é:Aqui você pode ver o compilador otimizando a chamada para
foo(input)
uma única instruçãoaddl %eax, %eax
, mas sem movê-la para fora do tempo ou eliminá-la inteiramente, apesar da entrada constante.Espero que isso ajude, e o comitê de padrões C ++ está analisando a possibilidade de padronizar APIs semelhantes a esta
DoNotOptimize
aqui.fonte
Clock::now()
sejam reordenadas em relação a foo ()? O optimzer tenho que assumir queDoNotOptimize
eClock::now()
ter acesso e pode modificar alguns estado global comum que por sua vez iria amarrá-los para a entrada e saída? Ou você está contando com algumas limitações atuais da implementação do otimizador?DoNotOptimize
neste exemplo, é um evento sinteticamente "observável". É como se ele imprimisse nocionalmente uma saída visível para algum terminal com a representação da entrada. Visto que a leitura do relógio também é observável (você está observando o tempo passando), eles não podem ser reordenados sem alterar o comportamento observável do programa.foo
função está fazendo algumas operações como ler de um soquete que pode estar bloqueado por um tempo, isso conta uma operação observável? E como aread
operação não é "totalmente conhecida" (certo?), O código se manterá em ordem?Resumo:
Parece não haver uma maneira garantida de evitar a reordenação, mas, desde que a otimização de tempo de link / programa completo não esteja habilitada, localizar a função chamada em uma unidade de compilação separada parece uma boa aposta . (Pelo menos com o GCC, embora a lógica sugira que isso seja provável com outros compiladores também.) Isso vem com o custo da chamada de função - o código embutido está por definição na mesma unidade de compilação e aberto para reordenamento.
Resposta original:
O GCC reordena as chamadas sob otimização de -O2:
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
:Mas:
g++ -S --std=c++11 -O2 fred.cpp
:Agora, com foo () como uma função externa:
g++ -S --std=c++11 -O2 fred.cpp
:MAS, se estiver vinculado a -flto (otimização de tempo de link):
fonte
A reordenação pode ser feita pelo compilador ou pelo processador.
A maioria dos compiladores oferece um método específico de plataforma para evitar a reordenação de instruções de leitura e gravação. No gcc, este é
( Mais informações aqui )
Observe que isso evita apenas indiretamente operações de reordenamento, desde que elas dependam de leituras / gravações.
Na prática , ainda não vi um sistema em que a chamada do sistema
Clock::now()
tenha o mesmo efeito que essa barreira. Você pode inspecionar a montagem resultante para ter certeza.Não é incomum, no entanto, que a função em teste seja avaliada durante o tempo de compilação. Para impor uma execução "realista", você pode precisar derivar a entrada
foo()
de E / S ou umavolatile
leitura.Outra opção seria desativar o inlining
foo()
- novamente, isso é específico do compilador e geralmente não é portátil, mas teria o mesmo efeito.No gcc, isso seria
__attribute__ ((noinline))
@Ruslan traz à tona uma questão fundamental: Quão realista é essa medição?
O tempo de execução é afetado por muitos fatores: um é o hardware real em que estamos executando, o outro é o acesso simultâneo a recursos compartilhados como cache, memória, disco e núcleos de CPU.
Portanto, o que geralmente fazemos para obter tempos comparáveis : certifique-se de que sejam reproduzíveis com uma margem de erro baixa. Isso os torna um tanto artificiais.
O desempenho de execução de "cache quente" vs. "cache frio" pode facilmente diferir em uma ordem de magnitude - mas na realidade, será algo intermediário ("morno"?)
fonte
asm
afeta o tempo de execução das instruções entre as chamadas do timer: o código após a alteração da memória deve recarregar todas as variáveis da memória.A linguagem C ++ define o que é observável de várias maneiras.
Se
foo()
não fizer nada observável, pode ser eliminado completamente. Sefoo()
apenas fizer um cálculo que armazena valores no estado "local" (seja na pilha ou em um objeto em algum lugar), e o compilador puder provar que nenhum ponteiro derivado de forma segura pode entrar noClock::now()
código, então não há consequências observáveis para mover asClock::now()
chamadas.Se
foo()
interagiu com um arquivo ou a tela, eo compilador não pode provar queClock::now()
faz não interagir com o arquivo ou a tela, então reordenamento não pode ser feito, porque a interação com um arquivo ou exibição é comportamento observável.Embora você possa usar hacks específicos do compilador para forçar o código a não se mover (como o assembly embutido), outra abordagem é tentar ser mais esperto que seu compilador.
Crie uma biblioteca carregada dinamicamente. Carregue-o antes do código em questão.
Essa biblioteca expõe uma coisa:
e o embrulha assim:
que empacota um lambda nulo e usa a biblioteca dinâmica para executá-lo em um contexto que o compilador não consegue entender.
Dentro da biblioteca dinâmica, fazemos:
o que é muito simples.
Agora, para reordenar as chamadas para
execute
, ele deve entender a biblioteca dinâmica, o que não pode acontecer durante a compilação do código de teste.Ele ainda pode eliminar
foo()
s sem efeitos colaterais, mas você ganha alguns e perde outros.fonte
volatile
acesso fictício ou chamada para um código externo.Não, não pode. De acordo com o padrão C ++ [introdução.execução]:
Uma expressão completa é basicamente uma instrução terminada por um ponto e vírgula. Como você pode ver, a regra acima estipula que as instruções devem ser executadas em ordem. É dentro das instruções que o compilador tem mais rédeas soltas (ou seja, é permitido, sob alguma circunstância, avaliar expressões que compõem uma instrução em ordens diferentes da esquerda para a direita ou qualquer outra coisa específica).
Observe que as condições para a aplicação da regra de as-if não são atendidas aqui. Não é razoável pensar que qualquer compilador seria capaz de provar que reordenar chamadas para obter a hora do sistema não afetaria o comportamento observável do programa. Se houvesse uma circunstância em que duas chamadas para obter o tempo pudessem ser reordenadas sem alterar o comportamento observado, seria extremamente ineficiente produzir de fato um compilador que analisasse um programa com compreensão suficiente para ser capaz de inferir isso com certeza.
fonte
Não.
Às vezes, pela regra "como se", as instruções podem ser reordenadas. Isso não ocorre porque eles são logicamente independentes um do outro, mas porque essa independência permite que tal reordenação ocorra sem alterar a semântica do programa.
Mover uma chamada do sistema que obtém a hora atual obviamente não satisfaz essa condição. Um compilador que faz isso consciente ou inconscientemente não é compatível e é realmente bobo.
Em geral, não espero que qualquer expressão que resulte em uma chamada de sistema seja "adivinhada", mesmo por um compilador de otimização agressiva. Ele simplesmente não sabe o suficiente sobre o que essa chamada de sistema faz.
fonte
int x = 0; clock(); x = y*2; clock();
não há maneiras definidas de oclock()
código interagir com o estado dex
. Sob o padrão C ++, ele não precisa saber o queclock()
faz - ele pode examinar a pilha (e perceber quando o cálculo ocorre), mas esse não é o problema do C ++ .t2
e do segundot1
, seria não conforme e tolo se esses valores fossem usados, o que esta resposta falha é que um compilador em conformidade pode às vezes reordenar outro código em uma chamada de sistema. Nesse caso, desde que ele saiba o quefoo()
faz (por exemplo, porque ele o incluiu) e, portanto, que (em termos gerais) seja uma função pura, ele poderá movê-lo.y*y
antes da chamada do sistema, apenas por diversão. Também não há garantia de que a implementação real não usará o resultado desse cálculo especulativo posteriormente em qualquer pontox
usado, portanto, não fará nada entre as chamadas declock()
. O mesmo vale para tudo o que uma função embutidafoo
faz, desde que não tenha efeitos colaterais e não possa depender do estado que pode ser alterado porclock()
.noinline
função + caixa preta de montagem embutida + dependências de dados completosIsso se baseia em https://stackoverflow.com/a/38025837/895245, mas como não vi nenhuma justificativa clara de por que o
::now()
não pode ser reordenado lá, prefiro ser paranóico e colocá-lo dentro de uma função noinline junto com o asm.Dessa forma, tenho certeza que o reordenamento não pode acontecer, pois o
noinline
"amarra" o::now
e a dependência de dados.main.cpp
GitHub upstream .
Compile e execute:
A única desvantagem menor desse método é que adicionamos uma
callq
instrução extra sobre uminline
método.objdump -CD
mostra quemain
contém:então vemos que
foo
estava alinhado, masget_clock
não estava e o cercava.get_clock
em si, entretanto, é extremamente eficiente, consistindo em uma única instrução otimizada de chamada de folha que nem chega à pilha:Como a precisão do relógio é limitada, acho improvável que você consiga notar os efeitos de tempo de um extra
jmpq
. Observe que umcall
é necessário independentemente, pois::now()
está em uma biblioteca compartilhada.Chamada
::now()
de assembly embutido com dependência de dadosEssa seria a solução mais eficiente possível, superando até mesmo o extra
jmpq
mencionado acima.Infelizmente, isso é extremamente difícil de fazer corretamente, conforme mostrado em: Chamando printf no ASM embutido estendido
Se a sua medição de tempo pode ser feita diretamente na montagem em linha sem uma chamada, entretanto, esta técnica pode ser usada. Este é o caso, por exemplo, das instruções de instrumentação mágica gem5 , x86 RDTSC (não tenho certeza se isso é mais representativo) e possivelmente outros contadores de desempenho.
Tópicos relacionados:
Testado com GCC 8.3.0, Ubuntu 19.04.
fonte
"+m"
, o"+r"
é uma maneira muito mais eficiente de fazer o compilador materializar um valor e então assumir que a variável mudou.