Como corretamente referência a [templated] programa C ++
-
22-07-2019 - |
Pergunta
Eu estou em um ponto onde eu realmente precisa para otimizar código C ++. Eu estou escrevendo uma biblioteca para simulações moleculares e eu preciso adicionar um novo recurso. Eu já tentei adicionar esta funcionalidade no passado, mas eu então utilizado funções virtuais chamados em loops aninhados. Eu tinha sentimentos ruins sobre isso e a primeira implementação provou que isso era uma má idéia. No entanto isso foi OK para testar o conceito.
Agora eu preciso que esse recurso esteja o mais rápido possível (bem, sem código de montagem ou cálculo GPU, isso ainda tem que ser C ++ e mais legível do que menos). Agora eu sei um pouco mais sobre modelos e políticas de classe (do excelente livro de Alexandrescu) e eu acho que a geração de código em tempo de compilação pode ser a solução.
No entanto eu preciso testar o projeto antes de fazer o enorme trabalho de implementá-lo para a biblioteca. A questão é sobre a melhor maneira de testar a eficácia deste novo recurso.
Obviamente eu preciso para transformar otimizações sobre porque sem este g ++ (e provavelmente outros compiladores também) iria manter algumas operações desnecessárias no código objeto. Eu também preciso fazer um uso pesado do novo recurso no benchmark, porque um delta de 1e-3 segundo pode fazer a diferença entre uma boa e uma má concepção (esse recurso será chamado milhão de vezes no programa real).
O problema é que g ++ às vezes é "muito inteligente", otimizando e pode remover um ciclo inteiro se considerar que o resultado de um cálculo nunca é utilizado. Eu já vi que uma vez que quando se olha para o código assembly de saída.
Se eu adicionar alguma impressão na saída padrão, o compilador será então forçado a fazer o cálculo no circuito, mas eu provavelmente principalmente referência a implementação iostream.
Então, como posso fazer uma correta ??strong> de referência de um recurso pouco extraído de uma biblioteca? questão relacionada: é a abordagem correta para fazer este tipo de in vitro testes em uma pequena unidade ou eu preciso todo o contexto
?Obrigado por conselhos!
Parece haver várias estratégias, de opções específicas do compilador permitindo ajuste fino para soluções mais gerais que devem funcionar com cada compilador como volatile
ou extern
.
Eu acho que vou tentar tudo isso. Muito obrigado por todas as suas respostas!
Solução
Se você quiser força qualquer compilador para não descartar um resultado, tem que escrever o resultado para um objeto volátil. Essa operação não pode ser otimizado para fora, por definição.
template<typename T> void sink(T const& t) {
volatile T sinkhole = t;
}
Sem sobrecarga iostream, apenas uma cópia que tem que permanecer no código gerado. Agora, se você está coletando os resultados de uma série de operações, é melhor não descartá-los um por um. Estas cópias ainda pode adicionar alguma sobrecarga. Em vez disso, recolher alguma forma todos os resultados em um único objeto não-volátil (por isso são necessários todos os resultados individuais) e depois atribuir esse objeto resultado para um volátil. Por exemplo. se suas operações individuais todas as cordas de produzir, você pode forçar a avaliação adicionando todos os valores de char juntos módulo 1 << 32. Isso adiciona quase nenhuma sobrecarga; as cordas provavelmente será em cache. O resultado da adição será posteriormente atribuído-a-volátil de modo que cada caractere em cada um deve picar, de facto, ser calculada, há atalhos permitido.
Outras dicas
A menos que você tem um realmente compilador agressivo (pode acontecer), eu sugiro calcular uma soma de verificação (basta adicionar todos os resultados juntos) e saída a soma de verificação.
Além disso, você pode querer olhar para o código assembly gerado antes de executar qualquer referência para que você possa verificar visualmente que quaisquer laços estão realmente sendo executado.
Compiladores só é permitido para eliminar código-ramos que não pode acontecer. Contanto que não se pode excluir que uma filial deve ser executado, ele não vai eliminá-lo. Contanto que haja alguma em algum lugar dependência de dados, o código vai estar lá e vai ser executado. Compiladores não são muito inteligentes sobre como estimar quais aspectos de um programa não será executado e não tente, porque isso é um problema NP e dificilmente calculável. Eles têm algumas verificações simples, como para if (0)
, mas isso é sobre isso.
A minha humilde opinião é que você estava possivelmente atingida por algum outro problema anteriormente, tais como o caminho C / C ++ avalia expressões booleanas.
Mas de qualquer maneira, uma vez que trata-se de um teste de velocidade, você pode verificar que as coisas são chamados para si mesmo - executá-lo uma vez sem, depois outra vez com um teste de valores de retorno. Ou uma variável estática sendo incrementado. No final do teste, imprimir o número gerado. Os resultados serão iguais.
Para responder à sua pergunta sobre testes in vitro in-: Sim, faça isso. Se seu aplicativo é tão tempo crítico, fazer isso. Por outro lado, a sua descrição aponta para um problema diferente: se os seus deltas estão em um prazo de 1E-3 segundos, em seguida, que soa como um problema de complexidade computacional, uma vez que o método em questão deve ser chamado muito, muito frequentemente (para algumas corridas, 1e-3 segundos é neglectible).
O domínio do problema que você está modelando sons muito complexo e os conjuntos de dados são provavelmente enorme. Essas coisas são sempre um esforço interessante. Certifique-se que você absolutamente tem as estruturas de dados corretos e algoritmos em primeiro lugar, porém, e micro-otimizar tudo o que quiser depois disso. Então, eu diria que olhar para todo o contexto primeira ; -).
Por curiosidade, qual é o problema que você está calculando?
Você tem um monte de controle sobre as otimizações para a sua compilação. -O1, -O2, e assim por diante são apenas aliases para um monte de switches.
A partir das páginas man
-O2 turns on all optimization flags specified by -O. It also turns
on the following optimization flags: -fthread-jumps -falign-func‐
tions -falign-jumps -falign-loops -falign-labels -fcaller-saves
-fcrossjumping -fcse-follow-jumps -fcse-skip-blocks
-fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
-fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove -fre‐
order-blocks -freorder-functions -frerun-cse-after-loop
-fsched-interblock -fsched-spec -fschedule-insns -fsched‐
ule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
-ftree-vrp
Você pode ajustar e usar este comando para ajudar a diminuir as opções para investigar.
...
Alternatively you can discover which binary optimizations are
enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts Φ grep enabled
Depois de encontrar a otimização culpret você não deve precisar do cout.
Se isto é possível para você, você pode tentar rachar seu código em:
- a biblioteca pretende testar compilado com todas as otimizações ligado
- um programa de teste, dinamically ligando a biblioteca, com otimizações desligado
Caso contrário, você pode especificar um nível de otimização diferentes (parece que você está usando gcc ...) para a functio teste n com o atributo optimize (veja http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes ) .
Você pode criar uma função fictícia em um arquivo cpp separado que não faz nada, mas tem como argumento qualquer que seja o tipo do seu resultado do cálculo. Então você pode chamar essa função com os resultados de seu cálculo, forçando gcc para gerar o código intermediário, ea única pena é o custo de invocar uma função (que não deve distorcer os resultados a menos que você chamá-lo de um muito! ).
#include <iostream>
// Mark coords as extern.
// Compiler is now NOT allowed to optimise away coords
// This it can not remove the loop where you initialise it.
// This is because the code could be used by another compilation unit
extern double coords[500][3];
double coords[500][3];
int main()
{
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
std::cout << "hello world !"<< std::endl;
return 0;
}
Editar : a coisa mais fácil que você pode fazer é simplesmente usar os dados de alguma forma espúria depois que a função foi executada e fora seus benchmarks. Como,
StartBenchmarking(); // ie, read a performance counter
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
StopBenchmarking(); // what comes after this won't go into the timer
// this is just to force the compiler to use coords
double foo;
for (int j = 0 ; j < 500 ; ++j )
{
foo += coords[j][0] + coords[j][1] + coords[j][2];
}
cout << foo;
O que às vezes funciona para mim nestes casos é para esconder o in vitro teste dentro de uma função e passar os conjuntos de dados de referência através de voláteis ponteiros. Isso diz ao compilador que ele não deve entrar em colapso gravações posteriores a esses ponteiros (porque eles podem ser ex de memória mapeada I / O). Então,
void test1( volatile double *coords )
{
//perform a simple initialization of all coordinates:
for (int i=0; i<1500; i+=3)
{
coords[i+0] = 3.23;
coords[i+1] = 1.345;
coords[i+2] = 123.998;
}
}
Por alguma razão eu não descobri ainda que nem sempre funciona em MSVC, mas que muitas vezes faz - olhar para a saída de montagem para ter certeza. Lembre-se também que volátil irá frustrar algumas otimizações do compilador (que proíbe o compilador de manter o conteúdo do ponteiro no registo e as forças escreve a ocorrer no fim do programa) de modo que este só é confiável, se você estiver usando-o para o write-out final dos dados.
Em geral in vitro testes como este é muito útil, desde que você lembre-se que não é toda a história. Eu costumo testar minhas novas rotinas de matemática de forma isolada como esta para que eu possa rapidamente iterate em apenas cache e dutos características do meu algoritmo em dados consistentes.
A diferença entre tubo de ensaio de perfis como este e executá-lo no "mundo real" significa que você vai ficar extremamente diferentes conjuntos de dados de entrada (melhor caso, às vezes, às vezes pior caso, às vezes patológica), o cache será em algum desconhecido declarar ao entrar na função, e você pode ter outros tópicos batendo no ônibus; por isso você deve executar alguns benchmarks sobre esta função in vivo , bem como quando você está acabado.
Eu não sei se GCC tem um recurso similar, mas com VC ++ você pode usar:
#pragma optimize
para ligar seletivamente otimizações ON / OFF. Se GCC tem capacidades semelhantes, você poderia construir com otimização plena e apenas desligá-lo quando necessário, para garantir que o seu código é chamado.
Apenas um pequeno exemplo de uma otimização indesejado:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
double coords[500][3];
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
cout << "hello world !"<< endl;
return 0;
}
Se você comentar o código da "coords duplas [500] [3]" para o fim do loop vai gerar exatamente o mesmo código de montagem (apenas tentei com g ++ 4.3.2). Eu sei que este exemplo é muito simples, e eu não era capaz de mostrar esse comportamento com um std :: vector de um simples "Coordenadas" estrutura.
No entanto, eu acho que este exemplo mostra ainda que algumas otimizações podem introduzir erros no benchmark e eu queria evitar algumas surpresas deste tipo ao introduzir o novo código em uma biblioteca. É fácil imaginar que o novo contexto pode impedir que algumas otimizações e levar a uma biblioteca muito ineficiente.
O mesmo deve também aplicar-se com funções virtuais (mas eu não provar isso aqui). Usado em um contexto onde um vínculo estático iria fazer o trabalho que estou bastante confiante de que compiladores decentes deve eliminar a chamada indireta extra para a função virtual. Posso tentar esta chamada em um loop e concluir que chamar uma função virtual não é um negócio tão grande. Então eu vou chamá-lo de centenas de milhares de vezes em um contexto em que o compilador não pode adivinhar o que vai ser o tipo exato do ponteiro e têm um aumento de 20% do tempo de execução ...
na inicialização, ler um arquivo. no seu código, dizer se (entrada == "x") cout << result_of_benchmark;
O compilador não será capaz de eliminar o cálculo, e se você garantir a entrada não é "x", você não vai aferir o iostream.