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 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!

Foi útil?

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.

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