Práticas de codificação que permitem ao compilador / otimizador tornar um programa mais rápido

116

Muitos anos atrás, os compiladores C não eram particularmente inteligentes. Para contornar o problema, K&R inventou a palavra-chave register , para sugerir ao compilador que talvez fosse uma boa ideia manter essa variável em um registro interno. Eles também fizeram o operador terciário para ajudar a gerar um código melhor.

Com o passar do tempo, os compiladores amadureceram. Eles se tornaram muito espertos em sua análise de fluxo, permitindo-lhes tomar melhores decisões sobre quais valores manter nos registros do que você poderia fazer. A palavra-chave de registro tornou-se sem importância.

FORTRAN pode ser mais rápido que C para alguns tipos de operações, devido a problemas de alias . Em teoria, com uma codificação cuidadosa, pode-se contornar essa restrição para permitir que o otimizador gere código mais rápido.

Quais práticas de codificação estão disponíveis que podem permitir que o compilador / otimizador gere código mais rápido?

  • Identificar a plataforma e o compilador que você usa, seria apreciado.
  • Por que a técnica parece funcionar?
  • O código de amostra é incentivado.

Aqui está uma questão relacionada

[Editar] Esta questão não é sobre o processo geral de definir o perfil e otimizar. Suponha que o programa foi escrito corretamente, compilado com otimização total, testado e colocado em produção. Pode haver construções em seu código que impeçam o otimizador de fazer o melhor trabalho possível. O que você pode fazer para refatorar que irá remover essas proibições e permitir que o otimizador gere um código ainda mais rápido?

[Editar] Link relacionado ao deslocamento

EvilTeach
fonte
7
Pode ser um bom candidato para comunidade wiki imho, já que não há uma resposta definitiva 'única' para esta (interessante) questão ...
ChristopheD
Eu sinto falta disso todas as vezes. Obrigado por apontar isso.
EvilTeach de
Por 'melhor' você quer dizer simplesmente 'mais rápido' ou você tem outros critérios de excelência em mente?
Marco de alto desempenho
1
É muito difícil escrever um bom alocador de registro, especialmente portável, e a alocação de registro é absolutamente essencial para o desempenho e o tamanho do código. registerna verdade, tornou o código sensível ao desempenho mais portátil ao combater compiladores ruins.
Potatoswatter
1
@EvilTeach: wiki da comunidade não significa "nenhuma resposta definitiva", não é sinônimo de tag subjetiva. Wiki da comunidade significa que você deseja entregar sua postagem à comunidade para que outras pessoas possam editá-la. Não se sinta pressionado a fazer suas perguntas no wiki se não tiver vontade.
Julieta,

Respostas:

54

Grave em variáveis ​​locais e não em argumentos de saída! Isso pode ser uma grande ajuda para contornar lentidões de aliasing. Por exemplo, se o seu código for

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

o compilador não sabe que foo1! = barOut e, portanto, precisa recarregar foo1 a cada vez que passar pelo loop. Ele também não pode ler foo2 [i] até que a gravação em barOut seja concluída. Você pode começar a mexer com indicadores restritos, mas é tão eficaz (e muito mais claro) fazer isso:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

Parece bobo, mas o compilador pode ser muito mais inteligente ao lidar com a variável local, já que ela não pode se sobrepor na memória a nenhum dos argumentos. Isso pode ajudá-lo a evitar o temido load-hit-store (mencionado por Francis Boivin neste tópico).

celion
fonte
7
Isso tem a vantagem adicional de tornar as coisas mais fáceis de ler / entender para os programadores também, uma vez que eles também não precisam se preocupar com possíveis efeitos colaterais não óbvios.
Michael Burr
A maioria dos IDEs exibe variáveis ​​locais por padrão, portanto há menos digitação
EvilTeach
9
você também pode ativar essa otimização usando ponteiros restritos
Ben Voigt
4
@Ben - é verdade, mas acho que esse caminho é mais claro. Além disso, se a entrada e a saída se sobrepõem, acredito que o resultado não seja especificado com ponteiros restritos (provavelmente obterá um comportamento diferente entre depurar e liberar), embora dessa forma pelo menos seja consistente. Não me interpretem mal, eu gosto de usar a restrição, mas gosto de não precisar ainda mais.
celion
Você só precisa esperar que Foo não tenha uma operação de cópia definida que copie alguns meg de dados ;-)
Skizz,
76

Aqui está uma prática de codificação para ajudar o compilador a criar código rápido - qualquer linguagem, qualquer plataforma, qualquer compilador, qualquer problema:

Você não usar todos os truques inteligentes que a força, ou mesmo incentivar, o compilador para colocar as variáveis na memória (incluindo cache e registos) como achar melhor. Escreva primeiro um programa que seja correto e sustentável.

Em seguida, analise seu código.

Então, e somente então, você pode querer começar a investigar os efeitos de dizer ao compilador como usar a memória. Faça uma mudança de cada vez e meça seu impacto.

Espere ficar desapontado e ter que trabalhar muito mesmo para pequenas melhorias de desempenho. Compiladores modernos para linguagens maduras como Fortran e C são muito, muito bons. Se você leu o relato de um 'truque' para obter melhor desempenho do código, tenha em mente que os escritores do compilador também leram sobre ele e, se valer a pena, provavelmente o implementaram. Eles provavelmente escreveram o que você leu em primeiro lugar.

Marca de alto desempenho
fonte
20
Os desenvolvedores da Compiier têm tempo finito, assim como todo mundo. Nem todas as otimizações farão parte do compilador. Como &vs. %para potências de dois (raramente, ou nunca, otimizado, mas pode ter impactos significativos no desempenho). Se você ler um truque de desempenho, a única maneira de saber se funciona é fazer a mudança e medir o impacto. Nunca assuma que o compilador otimizará algo para você.
Dave Jarvis
22
& e% é praticamente sempre otimizado, junto com a maioria dos outros truques aritméticos baratos. O que não é otimizado é o caso do operando do lado direito sendo uma variável que acontece sempre ser uma potência de dois.
Potatoswatter de
8
Para esclarecer, pareço ter confundido alguns leitores: o conselho na prática de codificação que proponho é primeiro desenvolver um código direto que não faça uso de instruções de layout de memória para estabelecer uma linha de base de desempenho. Em seguida, experimente as coisas uma de cada vez e meça seu impacto. Não ofereci nenhum conselho sobre o desempenho das operações.
Marco de alto desempenho
17
Para constantes poder-de-dois n, substitui gcc % ncom & (n-1) mesmo quando otimização está desativado . Isso não é exatamente "raramente, se nunca" ...
Porculus
12
% NÃO PODE ser otimizado como e quando o tipo é assinado, devido às regras idiotas de C para divisão de inteiro negativo (arredondado para 0 e ter resto negativo, ao invés de arredondamento para baixo e sempre tendo resto positivo). E na maioria das vezes, codificadores ignorantes usam tipos assinados ...
R .. GitHub PARE DE AJUDAR O ICE
47

A ordem em que você atravessa a memória pode ter impactos profundos no desempenho, e os compiladores não são muito bons em descobrir e consertar isso. Você deve estar atento às questões de localidade do cache ao escrever o código, se você se preocupa com o desempenho. Por exemplo, matrizes bidimensionais em C são alocadas no formato de linha principal. Percorrer matrizes no formato principal da coluna tenderá a fazer com que você tenha mais perdas de cache e a tornar seu programa mais limitado à memória do que ao processador:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}
vicatcu
fonte
Estritamente falando, este não é um problema do otimizador, mas sim de otimização.
EvilTeach de
10
Claro que é um problema do otimizador. As pessoas têm escrito artigos sobre otimização automática de intercâmbio de loop por décadas.
Phil Miller de
20
@Potatoswatter Do que você está falando? O compilador C pode fazer o que quiser, desde que o mesmo resultado final seja observado, e de fato o GCC 4.4 fez o -floop-interchangeque inverterá um loop interno e externo se o otimizador considerar lucrativo.
efemiente de
2
Huh, bem, aí está. A semântica C geralmente é prejudicada por problemas de aliasing. Acho que o verdadeiro conselho aqui é passar essa bandeira!
Potatoswatter
36

Otimizações genéricas

Aqui estão algumas das minhas otimizações favoritas. Na verdade, aumentei os tempos de execução e reduzi os tamanhos dos programas usando isso.

Declarar pequenas funções como inlineou macros

Cada chamada a uma função (ou método) incorre em sobrecarga, como colocar variáveis ​​na pilha. Algumas funções também podem gerar uma sobrecarga no retorno. Uma função ou método ineficiente tem menos instruções em seu conteúdo do que a sobrecarga combinada. Esses são bons candidatos para inlining, seja como #definemacros ou inlinefunções. (Sim, eu sei que inlineé apenas uma sugestão, mas neste caso eu considero um lembrete para o compilador).

Remova o código morto e redundante

Se o código não for usado ou não contribuir para o resultado do programa, livre-se dele.

Simplifique o design de algoritmos

Certa vez, removi muito código de montagem e tempo de execução de um programa, escrevendo a equação algébrica que ele estava calculando e, em seguida, simplifiquei a expressão algébrica. A implementação da expressão algébrica simplificada ocupou menos espaço e tempo do que a função original.

Loop Unrolling

Cada loop tem uma sobrecarga de incremento e verificação de término. Para obter uma estimativa do fator de desempenho, conte o número de instruções no overhead (mínimo 3: incremento, verificação, vá para o início do loop) e divida pelo número de instruções dentro do loop. Quanto menor o número, melhor.

Editar: forneça um exemplo de desenrolamento de loop Antes:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Após desenrolar:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Com essa vantagem, um benefício secundário é obtido: mais instruções são executadas antes que o processador precise recarregar o cache de instruções.

Tive resultados incríveis quando desenrolei um loop para 32 declarações. Esse foi um dos gargalos, pois o programa teve que calcular uma soma de verificação em um arquivo de 2 GB. Essa otimização combinada com a leitura de blocos melhorou o desempenho de 1 hora para 5 minutos. O desenrolamento de loops também forneceu um desempenho excelente em linguagem assembly, mas memcpyfoi muito mais rápido que o compilador memcpy. - TM

Redução de ifdeclarações

Os processadores detestam desvios ou saltos, uma vez que força o processador a recarregar sua fila de instruções.

Aritmética booleana ( editado: formato de código aplicado ao fragmento de código, exemplo adicionado)

Converta ifinstruções em atribuições booleanas. Alguns processadores podem executar instruções condicionalmente sem ramificação:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

O curto-circuito do operador lógico AND ( &&) impede a execução dos testes se o statusfor false.

Exemplo:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

Alocação de variável de fator fora dos loops

Se uma variável for criada rapidamente dentro de um loop, mova a criação / alocação para antes do loop. Na maioria dos casos, a variável não precisa ser alocada durante cada iteração.

Fatore as expressões constantes fora dos loops

Se um cálculo ou valor de variável não depende do índice do loop, mova-o para fora (antes) do loop.

I / O em blocos

Leia e grave dados em grandes blocos (blocos). Quanto maior melhor. Por exemplo, ler um octeto por vez é menos eficiente do que ler 1.024 octetos com uma leitura.
Exemplo:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

A eficiência desta técnica pode ser demonstrada visualmente. :-)

Não use printf família para dados constantes

Dados constantes podem ser produzidos usando uma gravação de bloco. A gravação formatada perderá tempo examinando o texto em busca de caracteres de formatação ou processando comandos de formatação. Veja o exemplo de código acima.

Formate na memória e depois escreva

Formate em uma charmatriz usando vários e sprintf, em seguida, use fwrite. Isso também permite que o layout de dados seja dividido em "seções constantes" e seções variáveis. Pense em mala direta .

Declare texto constante (literais de string) como static const

Quando as variáveis ​​são declaradas sem o static, alguns compiladores podem alocar espaço na pilha e copiar os dados da ROM. Essas são duas operações desnecessárias. Isso pode ser corrigido usando o staticprefixo.

Por último, o código como o compilador faria

Às vezes, o compilador pode otimizar várias pequenas instruções melhor do que uma versão complicada. Além disso, escrever código para ajudar a otimizar o compilador também ajuda. Se eu quiser que o compilador use instruções especiais de transferência de bloco, vou escrever um código que parece que deve usar as instruções especiais.

Thomas Matthews
fonte
2
Interessante, você pode fornecer um exemplo em que obteve um código melhor com algumas pequenas instruções, em vez de um maior. Você pode mostrar um exemplo de reescrita de um if usando booleanos. Geralmente, eu deixaria o loop se desenrolando para o compilador, pois provavelmente ele tem uma noção melhor do tamanho do cache. Estou um pouco surpreso com a ideia de sprintfing, depois fwriting. Eu acho que fprintf realmente faz isso por baixo do capô. Você pode dar mais detalhes aqui?
EvilTeach
1
Não há garantia de que os fprintfformatos em um buffer separado gerem a saída do buffer. Um simplificado (para uso de memória) fprintfproduziria todo o texto não formatado, em seguida, formataria e geraria, e repetiria até que toda a string de formato fosse processada, fazendo assim 1 chamada de saída para cada tipo de saída (formatado vs. não formatado). Outras implementações precisariam alocar memória dinamicamente para cada chamada para conter a nova string inteira (o que é ruim no ambiente de sistemas embarcados). Minha sugestão reduz o número de saídas.
Thomas Matthews
3
Certa vez, obtive uma melhoria significativa de desempenho enrolando um loop. Então descobri como enrolá-lo com mais força usando alguma indireção, e o programa ficou visivelmente mais rápido. (A criação de perfil mostrou que essa função específica era 60-80% do tempo de execução e testei o desempenho cuidadosamente antes e depois.) Acredito que a melhoria foi devido à melhor localidade, mas não estou completamente certo disso.
David Thornley,
16
Muitas delas são otimizações de programador, e não maneiras de os programadores ajudarem o compilador na otimização, que era o ponto principal da questão original. Por exemplo, desenrolamento de loop. Sim, você pode fazer seu próprio desenrolamento, mas acho mais interessante descobrir quais obstáculos existem para o desenrolamento e remoção do compilador para você.
Adrian McCarthy
26

O otimizador não está realmente no controle do desempenho do seu programa, você está. Use algoritmos e estruturas apropriados e perfil, perfil, perfil.

Dito isso, você não deve fazer um loop interno em uma pequena função de um arquivo em outro arquivo, pois isso impede que seja embutido.

Evite pegar o endereço de uma variável, se possível. Pedir um ponteiro não está "livre", pois significa que a variável precisa ser mantida na memória. Mesmo uma matriz pode ser mantida em registros se você evitar ponteiros - isso é essencial para vetorizar.

O que nos leva ao próximo ponto, leia o manual ^ # $ @ ! O GCC pode vetorizar código C simples se você espalhar um __restrict__aqui e outro __attribute__( __aligned__ )ali. Se você deseja algo muito específico do otimizador, talvez seja necessário ser específico.

Potatoswatter
fonte
14
Essa é uma boa resposta, mas observe que a otimização de todo o programa está se tornando mais popular e pode, de fato, funcionar em linha nas unidades de tradução.
Phil Miller de
1
@Novelocrat Sim - nem preciso dizer que fiquei muito surpreso na primeira vez em que vi algo de A.centrar no B.c.
Jonathon Reinhart
18

Na maioria dos processadores modernos, o maior gargalo é a memória.

Aliasing: Load-Hit-Store pode ser devastador em um loop fechado. Se você está lendo um local da memória e escrevendo em outro e sabe que eles são separados, colocar cuidadosamente uma palavra-chave alias nos parâmetros da função pode realmente ajudar o compilador a gerar um código mais rápido. No entanto, se as regiões de memória se sobrepõem e você usou 'alias', você terá uma boa sessão de depuração de comportamentos indefinidos!

Cache-miss: Não tenho certeza de como você pode ajudar o compilador, já que é principalmente algorítmico, mas há intrínsecos para pré-buscar memória.

Também não tente converter valores de ponto flutuante em int e vice-versa muito, pois eles usam registradores diferentes e a conversão de um tipo para outro significa chamar a instrução de conversão real, gravando o valor na memória e lendo-o de volta no conjunto de registradores adequado .

Francis Boivin
fonte
4
+1 para load-hit-stores e diferentes tipos de registro. Não tenho certeza de quão importante é no x86, mas eles estão devestando no PowerPC (por exemplo, Xbox360 e Playstation3).
celion
A maioria dos artigos sobre técnicas de otimização de loop do compilador pressupõe aninhamento perfeito, o que significa que o corpo de cada loop, exceto o mais interno, é apenas outro loop. Esses artigos simplesmente não discutem as etapas necessárias para generalizar tal, mesmo que seja muito claro que podem ser. Portanto, eu esperaria que muitas implementações não suportassem realmente essas generalizações, devido ao esforço extra envolvido. Portanto, os muitos algoritmos para otimizar o uso do cache em loops podem funcionar muito melhor em ninhos perfeitos do que em ninhos imperfeitos.
Phil Miller
11

A grande maioria do código que as pessoas escrevem será limitado por E / S (acredito que todo o código que escrevi por dinheiro nos últimos 30 anos foi limitado), portanto, as atividades do otimizador para a maioria das pessoas serão acadêmicas.

No entanto, gostaria de lembrar às pessoas que para o código ser otimizado, você tem que dizer ao compilador para otimizá-lo - muitas pessoas (inclusive eu quando me esqueço) postam benchmarks de C ++ aqui que não fazem sentido sem o otimizador estar habilitado.

anon
fonte
7
Eu confesso que sou peculiar - eu trabalho em grandes códigos de processamento de números científicos que são limitados pela largura de banda da memória. Para a população geral dos programas, concordo com Neil.
Marco de alto desempenho
6
Verdade; mas uma grande parte desse código vinculado a E / S hoje em dia é escrito em linguagens que são praticamente pessimizadores - linguagens que nem mesmo têm compiladores. Eu suspeito que as áreas onde C e C ++ ainda são usados ​​tendem a ser áreas onde é mais importante otimizar algo (uso da CPU, uso da memória, tamanho do código ...)
Porculus
3
Passei a maior parte dos últimos 30 anos trabalhando em código com muito pouca E / S. Economize por 2 anos fazendo banco de dados. Gráficos, sistemas de controle, simulação - nada disso vinculado a E / S. Se I / O fosse o gargalo da maioria das pessoas, não prestaríamos muita atenção à Intel e à AMD.
phkahler
2
Sim, eu realmente não acredito nesse argumento - caso contrário, nós (no meu trabalho) não estaríamos procurando maneiras de gastar mais tempo de computação também fazendo E / S. Além disso, muito do software vinculado a E / S que encontrei foi vinculado a E / S porque a E / S foi feita de maneira descuidada; se alguém otimiza os padrões de acesso (assim como com a memória), pode obter enormes ganhos de desempenho.
dash-tom-bang,
3
Recentemente, descobri que quase nenhum código escrito na linguagem C ++ é vinculado a E / S. Claro, se você está chamando uma função do sistema operacional para transferência de disco em massa, seu thread pode entrar em espera de E / S (mas com cache, mesmo isso é questionável). Mas as funções usuais da biblioteca de E / S, aquelas que todo mundo recomenda porque são padrão e portáteis, são na verdade miseravelmente lentas em comparação com a tecnologia de disco moderna (mesmo as coisas de preço moderado). Provavelmente, a E / S é o gargalo apenas se você estiver liberando todo o caminho para o disco depois de gravar apenas alguns bytes. OTOH, UI é uma questão diferente, nós humanos somos lentos.
Ben Voigt
11

use const correção tanto quanto possível em seu código. Ele permite que o compilador otimize muito melhor.

Neste documento há muitas outras dicas de otimização: otimizações de CPP (um documento um pouco antigo)

luzes:

  • usar listas de inicialização de construtor
  • usar operadores de prefixo
  • use construtores explícitos
  • funções inline
  • evite objetos temporários
  • esteja ciente do custo das funções virtuais
  • retornar objetos por meio de parâmetros de referência
  • considere a alocação por classe
  • considere alocadores de contêineres stl
  • a otimização de 'membro vazio'
  • etc
Toad
fonte
8
Não muito, raramente. No entanto, melhora a exatidão real.
Potatoswatter de
5
Em C e C ++, o compilador não pode usar const para otimizar porque descartá-lo é um comportamento bem definido.
dsimcha de
+1: const é um bom exemplo de algo que impactará diretamente o código compilado. Comentário re @ dsimcha - um bom compilador testará para ver se isso acontece. Claro, um bom compilador irá "encontrar" elementos const que não são declarados dessa forma de qualquer maneira ...
Hogan
@dsimcha: Modificação de um const e restrict qualificada ponteiro, no entanto, não está definida. Portanto, um compilador pode otimizar de forma diferente nesse caso.
Dietrich Epp
6
@dsimcha lançar fora constde uma constreferência ou constponteiro para um constobjeto não é bem definido. modificar um constobjeto real (ou seja, um declarado como constoriginalmente) não é.
Stephen Lin
9

Tente programar usando atribuição única estática, tanto quanto possível. SSA é exatamente o mesmo que você obtém na maioria das linguagens de programação funcionais, e é para isso que a maioria dos compiladores converte seu código para fazer suas otimizações, porque é mais fácil de trabalhar. Ao fazer isso, locais onde o compilador pode ficar confuso são trazidos à luz. Ele também faz com que todos os alocadores de registro, exceto os piores, funcionem tão bem quanto os melhores alocadores de registro, e permite depurar mais facilmente porque você quase nunca precisa se perguntar de onde uma variável obteve seu valor, pois havia apenas um lugar onde ela foi atribuída.
Evite variáveis ​​globais.

Ao trabalhar com dados por referência ou ponteiro, coloque-os nas variáveis ​​locais, faça seu trabalho e copie-os de volta. (a menos que você tenha um bom motivo para não)

Use a comparação quase gratuita com 0 que a maioria dos processadores fornece ao fazer operações matemáticas ou lógicas. Quase sempre você obtém um sinalizador para == 0 e <0, dos quais pode facilmente obter 3 condições:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

quase sempre é mais barato do que testar outras constantes.

Outro truque é usar a subtração para eliminar uma comparação no teste de alcance.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

Isso muitas vezes pode evitar um salto em linguagens que fazem curto-circuito em expressões booleanas e evita que o compilador tenha que tentar descobrir como lidar com o acompanhamento do resultado da primeira comparação enquanto faz a segunda e depois os combina. Pode parecer que tem o potencial de usar um registro extra, mas quase nunca o faz. Freqüentemente, você não precisa mais do foo e, se precisar, o rc ainda não é usado, então ele pode ir para lá.

Ao usar as funções de string em c (strcpy, memcpy, ...) lembre-se do que elas retornam - o destino! Freqüentemente, você pode obter um código melhor 'esquecendo' sua cópia do ponteiro para o destino e apenas recuperá-lo do retorno dessas funções.

Nunca ignore a oportunidade de retornar exatamente a mesma coisa que a última função que você chamou retornou. Compiladores não são tão bons em pegar isso:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

Claro, você poderia inverter a lógica disso se e apenas tivesse um ponto de retorno.

(truques que lembrei mais tarde)

Declarar funções como estáticas quando possível é sempre uma boa ideia. Se o compilador puder provar a si mesmo que foi responsável por cada chamador de uma função específica, ele pode quebrar as convenções de chamada para essa função em nome da otimização. Os compiladores geralmente podem evitar mover parâmetros para registradores ou posições de pilha em que as funções chamadas normalmente esperam que seus parâmetros estejam (para fazer isso, é necessário desviar tanto na função chamada quanto na localização de todos os chamadores). O compilador também pode aproveitar a vantagem de saber qual memória e registros a função chamada precisará e evitar a geração de código para preservar valores de variáveis ​​que estão em registros ou locais de memória que a função chamada não perturba. Isso funciona especialmente bem quando há poucas chamadas para uma função.

nategoose
fonte
2
Na verdade, não é necessário usar subtração ao testar intervalos, LLVM, GCC e meu compilador pelo menos fazem isso automaticamente. Poucas pessoas provavelmente entenderiam o que o código com subtração faz e menos ainda por que ele realmente funciona.
Gratian Lup
no exemplo acima, b () não pode ser chamado porque se (x <0) então a () será chamado.
EvilTeach
@EvilTeach Não, não vai. A comparação que resulta na chamada para a () é! X
nategoose de
@nategoose. se x for -3, então! x é verdadeiro.
EvilTeach
@EvilTeach Em C 0 é falso e tudo o mais é verdadeiro, então -3 é verdadeiro, então! -3 é falso
nategoose
9

Eu escrevi um compilador C otimizado e aqui estão algumas coisas muito úteis a serem consideradas:

  1. Torne a maioria das funções estáticas. Isso permite que a propagação da constante interprocedural e a análise de alias façam seu trabalho, caso contrário, o compilador precisa presumir que a função pode ser chamada de fora da unidade de tradução com valores completamente desconhecidos para os parâmetros. Se você olhar para as bibliotecas de código aberto bem conhecidas, todas marcam funções como estáticas, exceto aquelas que realmente precisam ser externas.

  2. Se variáveis ​​globais forem usadas, marque-as como estáticas e constantes, se possível. Se eles forem inicializados uma vez (somente leitura), é melhor usar uma lista de inicializadores como static const int VAL [] = {1,2,3,4}, caso contrário, o compilador pode não descobrir que as variáveis ​​são realmente constantes inicializadas e não conseguirá substituir as cargas da variável pelas constantes.

  3. NUNCA use um goto para dentro de um loop, o loop não será mais reconhecido pela maioria dos compiladores e nenhuma das otimizações mais importantes será aplicada.

  4. Use parâmetros de ponteiro apenas se necessário e marque-os como restritos, se possível. Isso ajuda muito a análise de alias porque o programador garante que não há alias (a análise de alias interprocedural geralmente é muito primitiva). Objetos de estrutura muito pequenos devem ser passados ​​por valor, não por referência.

  5. Use arrays em vez de ponteiros sempre que possível, especialmente dentro de loops (a [i]). Um array geralmente oferece mais informações para análise de alias e, após algumas otimizações, o mesmo código será gerado de qualquer maneira (pesquise por redução de força de loop se estiver curioso). Isso também aumenta a chance de o movimento do código invariante do loop ser aplicado.

  6. Tente içar fora das chamadas de loop para funções grandes ou funções externas que não tenham efeitos colaterais (não dependa da iteração do loop atual). Em muitos casos, pequenas funções são embutidas ou convertidas em intrínsecos que são fáceis de içar, mas funções grandes podem parecer para o compilador ter efeitos colaterais, quando na verdade não têm. Os efeitos colaterais para funções externas são completamente desconhecidos, com exceção de algumas funções da biblioteca padrão que às vezes são modeladas por alguns compiladores, tornando possível o movimento do código invariável em loop.

  7. Ao escrever testes com várias condições, coloque o mais provável primeiro. if (a || b || c) deveria ser if (b || a || c) se b é mais provável de ser verdadeiro do que os outros. Os compiladores geralmente não sabem nada sobre os valores possíveis das condições e quais ramificações são mais usadas (eles podem ser conhecidos usando informações de perfil, mas poucos programadores as usam).

  8. Usar um switch é mais rápido do que fazer um teste como if (a || b || ... || z). Verifique primeiro se o seu compilador faz isso automaticamente, alguns fazem e é mais legível ter o if .

2 rotações
fonte
7

No caso de sistemas embarcados e código escrito em C / C ++, tento evitar a alocação dinâmica de memória o máximo possível. O principal motivo de eu fazer isso não é necessariamente o desempenho, mas essa regra prática tem implicações no desempenho.

Algoritmos usados ​​para gerenciar o heap são notoriamente lentos em algumas plataformas (por exemplo, vxworks). Pior ainda, o tempo que leva para retornar de uma chamada para malloc é altamente dependente do estado atual do heap. Portanto, qualquer função que chame malloc terá um impacto de desempenho que não pode ser facilmente contabilizado. Esse impacto no desempenho pode ser mínimo se o heap ainda estiver limpo, mas depois que o dispositivo for executado por um tempo, o heap pode ficar fragmentado. As chamadas vão demorar mais e você não pode calcular facilmente como o desempenho irá diminuir com o tempo. Você realmente não pode produzir uma estimativa de pior caso. O otimizador também não pode fornecer ajuda nesse caso. Para piorar as coisas, se o heap se tornar muito fragmentado, as chamadas começarão a falhar completamente. A solução é usar pools de memória (por exemplo,fatias glib ) em vez da pilha. As chamadas de alocação serão muito mais rápidas e determinísticas se você fizer isso da maneira certa.

figurassa
fonte
Minha regra é: se você tiver que alocar dinamicamente, obtenha um array para que não precise fazer isso de novo. Pré-aloque os vetores.
EvilTeach
7

Uma dica idiota, mas que vai economizar algumas quantidades microscópicas de velocidade e código.

Sempre passe os argumentos da função na mesma ordem.

Se você tiver f_1 (x, y, z) que chama f_2, declare f_2 como f_2 (x, y, z). Não o declare como f_2 (x, z, y).

A razão para isso é que a plataforma C / C ++ ABI (convenção de chamada também conhecida como convenção de chamada) promete passar argumentos em registradores específicos e locais de pilha. Quando os argumentos já estão nos registradores corretos, não é necessário movê-los.

Ao ler o código desmontado, vi alguns registros ridículos embaralhados porque as pessoas não seguiram essa regra.

Zan Lynx
fonte
2
Nem C nem C ++ oferecem qualquer garantia sobre, ou mesmo mencionam, a passagem de registradores ou locais de pilha específicos. É a ABI (por exemplo, Linux ELF) que determina os detalhes da passagem de parâmetros.
Emmet
5

Duas técnicas de codificação que não vi na lista acima:

Ignore o linker escrevendo o código como uma fonte única

Embora a compilação separada seja muito boa para o tempo de compilação, é muito ruim quando se fala em otimização. Basicamente, o compilador não pode otimizar além da unidade de compilação, que é o domínio reservado do vinculador.

Mas se você projetar bem seu programa, também poderá compilá-lo por meio de uma fonte comum única. Em vez de compilar unit1.ce unit2.c, vincule os dois objetos, compile all.c que simplesmente #inclui unit1.ce unit2.c. Assim, você se beneficiará de todas as otimizações do compilador.

É muito parecido com escrever cabeçalhos de programas apenas em C ++ (e ainda mais fácil de fazer em C).

Esta técnica é bastante fácil se você escrever seu programa para habilitá-lo desde o início, mas você também deve estar ciente de que ela altera parte da semântica C e você pode encontrar alguns problemas como variáveis ​​estáticas ou colisão de macro. Para a maioria dos programas, é fácil superar os pequenos problemas que ocorrem. Também esteja ciente de que a compilação como uma fonte única é muito mais lenta e pode consumir uma grande quantidade de memória (geralmente não é um problema com sistemas modernos).

Usando essa técnica simples, aconteceu que alguns programas que escrevi dez vezes mais rápido!

Como a palavra-chave register, esse truque também pode se tornar obsoleto em breve. A otimização através do linker começa a ser suportada pelos compiladores gcc: Link time optimization .

Tarefas atômicas separadas em loops

Este é mais complicado. É sobre a interação entre o projeto do algoritmo e a maneira como o otimizador gerencia o cache e a alocação de registros. Freqüentemente, os programas precisam fazer um loop sobre alguma estrutura de dados e, para cada item, executar algumas ações. Muitas vezes, as ações realizadas podem ser divididas entre duas tarefas logicamente independentes. Se for esse o caso, você pode escrever exatamente o mesmo programa com dois loops no mesmo limite executando exatamente uma tarefa. Em alguns casos, escrever desta forma pode ser mais rápido do que o loop exclusivo (os detalhes são mais complexos, mas uma explicação pode ser que com o caso de tarefa simples todas as variáveis ​​podem ser mantidas nos registros do processador e com o mais complexo não é possível e alguns registradores devem ser gravados na memória e lidos posteriormente e o custo é maior do que o controle de fluxo adicional).

Tenha cuidado com este (desempenho de perfil usando este truque ou não), pois como usar o registro, ele também pode fornecer desempenhos menores do que melhores.

kriss
fonte
2
Sim, até agora, LTO tornou a primeira metade deste post redundante e provavelmente um conselho ruim.
sublinhado_d
@underscore_d: ainda existem alguns problemas (principalmente relacionados à visibilidade dos símbolos exportados), mas de um mero ponto de vista do desempenho provavelmente não há mais necessidade.
Kriss
4

Na verdade, eu já vi isso feito no SQLite e eles afirmam que resulta em aumentos de desempenho de ~ 5%: Coloque todo o seu código em um arquivo ou use o pré-processador para fazer o equivalente a isso. Dessa forma, o otimizador terá acesso a todo o programa e poderá fazer mais otimizações interprocedurais.

dsimcha
fonte
5
Colocar funções que são usadas juntas em proximidade física na origem aumenta a probabilidade de que elas estejam próximas umas das outras em arquivos de objeto e próximas umas das outras em seu executável. Essa localização aprimorada de instruções pode ajudar a evitar perdas no cache de instruções durante a execução.
paxos1977 de
O compilador AIX possui um switch de compilador para encorajar esse comportamento -qipa [= <suboptions_list>] | -qnoipa Ativa ou personaliza uma classe de otimizações conhecida como análise interprocedural (IPA).
EvilTeach de
4
O melhor é ter uma maneira de desenvolver que não exija isso. Usar esse fato como uma desculpa para escrever código não modular geralmente resultará apenas em um código lento e com problemas de manutenção.
Hogan de
3
Acho que esta informação está um pouco desatualizada. Em teoria, os recursos de otimização de todo o programa embutidos em muitos compiladores agora (por exemplo, "Otimização de tempo de link" no gcc) permitem os mesmos benefícios, mas com um fluxo de trabalho totalmente padrão (mais tempos de recompilação mais rápidos do que colocar tudo em um arquivo !)
Ponkadoodle
@Wallacoloo Com certeza, isso é data faaar outta. FWIW, acabei de usar o LTO do GCC pela primeira vez hoje e - todo o resto sendo igual -O3- ele explodiu 22% do tamanho original do meu programa. (Não é limitado pela CPU, então não tenho muito a dizer sobre velocidade.)
sublinhado_d
4

A maioria dos compiladores modernos deve fazer um bom trabalho ao acelerar a recursão de cauda , porque as chamadas de função podem ser otimizadas.

Exemplo:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

Claro que este exemplo não tem nenhuma verificação de limites.

Edição Tardia

Embora eu não tenha conhecimento direto do código; parece claro que os requisitos de uso de CTEs no SQL Server foram projetados especificamente para que possam ser otimizados por meio da recursão final.

Hogan
fonte
1
a questão é sobre C. C não remove a recursão de cauda, ​​então cauda ou outra recursão, a pilha pode explodir se a recursão for muito profunda.
Toad de
1
Evitei o problema da convenção de chamada usando um goto. Dessa forma, há menos sobrecarga.
EvilTeach de
2
@hogan: isso é novo para mim. Você poderia apontar para algum compilador que faça isso? E como você pode ter certeza de que realmente o otimiza? Se isso acontecer, é preciso ter certeza de que o fará. Não é algo que você espera que o otimizador do compilador pegue (como inlining que pode ou não funcionar)
Toad
6
@hogan: Eu estou corrigido. Você está certo que Gcc e MSVC fazem otimização de recursão de cauda.
Toad de
5
Este exemplo não é recursão de cauda, ​​pois não é a chamada recursiva que é a última, é a multiplicação.
Brian Young
4

Não faça o mesmo trabalho repetidamente!

Um antipadrão comum que vejo segue estas linhas:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

Na verdade, o compilador precisa chamar todas essas funções o tempo todo. Supondo que você, o programador, saiba que o objeto agregado não está mudando no decorrer dessas chamadas, pelo amor de tudo que é sagrado ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

No caso do getter singleton, as chamadas podem não ser muito caras, mas certamente é um custo (normalmente, "verifique se o objeto foi criado, se não foi, crie-o e, em seguida, devolva-o). mais complicada se torna essa cadeia de getters, mais tempo perdido teremos.

dash-tom-bang
fonte
3
  1. Use o escopo mais local possível para todas as declarações de variáveis.

  2. Use constsempre que possível

  3. Não use o registro a menos que planeje criar um perfil com e sem ele

Os 2 primeiros, especialmente o 1, ajudam o otimizador a analisar o código. Isso o ajudará especialmente a fazer boas escolhas sobre quais variáveis ​​manter nos registros.

Usar cegamente a palavra-chave register pode tanto ajudar quanto prejudicar sua otimização. É muito difícil saber o que importa até você olhar para a saída ou perfil da montagem.

Existem outras coisas que são importantes para obter um bom desempenho do código; projetar suas estruturas de dados para maximizar a coerência do cache, por exemplo. Mas a questão era sobre o otimizador.

John Knoeller
fonte
3

Lembrei-me de algo que encontrei uma vez, em que o sintoma era simplesmente que estávamos ficando sem memória, mas o resultado foi um desempenho substancialmente aprimorado (bem como grandes reduções no consumo de memória).

O problema, neste caso, era que o software que estávamos usando fazia toneladas de pequenas alocações. Por exemplo, alocar quatro bytes aqui, seis bytes ali, etc. Muitos pequenos objetos, também, rodando na faixa de 8-12 bytes. O problema não era tanto que o programa precisasse de muitas pequenas coisas, mas que alocava várias pequenas coisas individualmente, o que aumentava cada alocação para (nesta plataforma em particular) 32 bytes.

Parte da solução foi montar um pool de pequenos objetos no estilo Alexandrescu, mas estendê-lo para que eu pudesse alocar matrizes de pequenos objetos, bem como itens individuais. Isso também ajudou imensamente no desempenho, pois mais itens cabem no cache ao mesmo tempo.

A outra parte da solução era substituir o uso excessivo de membros char * gerenciados manualmente por uma string SSO (otimização de string pequena). A alocação mínima sendo de 32 bytes, eu construí uma classe de string que tinha um buffer de 28 caracteres embutido atrás de um char *, então 95% de nossas strings não precisaram fazer uma alocação adicional (e então eu substituí manualmente quase todas as aparências de char * nesta biblioteca com esta nova classe, que foi divertida ou não). Isso também ajudou muito com a fragmentação da memória, o que aumentou a localidade de referência para outros objetos apontados e, da mesma forma, houve ganhos de desempenho.

dash-tom-bang
fonte
3

Uma técnica bacana que aprendi com o comentário de @MSalters sobre esta resposta permite que os compiladores façam a elisão de cópia mesmo ao retornar objetos diferentes de acordo com alguma condição:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;
Xeo
fonte
2

Se você tem pequenas funções que chama repetidamente, já tive grandes ganhos colocando-as nos cabeçalhos como "inline estático". Chamadas de função no ix86 são surpreendentemente caras.

Reimplementar funções recursivas de maneira não recursiva usando uma pilha explícita também pode ganhar muito, mas você realmente está no reino do tempo de desenvolvimento versus ganho.

Remy
fonte
Converter a recursão em pilha é uma otimização assumida no ompf.org, para pessoas que desenvolvem raytracers e escrevem outros algoritmos de renderização.
Tom,
... Devo acrescentar que a maior sobrecarga em meu projeto pessoal de raytracer é a recursão baseada em vtable por meio de uma hierarquia de volume delimitador usando o padrão Composite. Na verdade, é apenas um monte de caixas aninhadas estruturadas como uma árvore, mas usar o padrão causa inchaço de dados (ponteiros de tabela virtual) e reduz a coerência da instrução (o que poderia ser um loop pequeno / apertado agora é uma cadeia de chamadas de função)
Tom,
2

Aqui está meu segundo conselho de otimização. Tal como acontece com o meu primeiro conselho, este é um propósito geral, não é específico para linguagem ou processador.

Leia o manual do compilador completamente e entenda o que ele está dizendo a você. Use o compilador ao máximo.

Eu concordo com um ou dois dos outros entrevistados que identificaram a seleção do algoritmo certo como crítica para espremer o desempenho de um programa. Além disso, a taxa de retorno (medida na melhoria da execução do código) no tempo que você investe no uso do compilador é muito maior do que a taxa de retorno no ajuste do código.

Sim, os escritores de compiladores não são de uma raça de gigantes da codificação e os compiladores contêm erros e o que deveria, de acordo com o manual e a teoria do compilador, tornar as coisas mais rápidas às vezes torna as coisas mais lentas. É por isso que você deve dar um passo de cada vez e medir o desempenho antes e depois de ajustar.

E sim, no final das contas, você pode se deparar com uma explosão combinatória de sinalizadores do compilador, então você precisa ter um script ou dois para executar o make com vários sinalizadores do compilador, enfileirar os trabalhos no grande cluster e reunir as estatísticas de tempo de execução. Se for apenas você e o Visual Studio em um PC, você perderá o interesse muito antes de tentar combinações suficientes de sinalizadores de compilador suficientes.

Saudações

Marca

Quando eu pego um pedaço de código, geralmente posso obter um fator de 1,4 - 2,0 vezes mais desempenho (ou seja, a nova versão do código é executada em 1 / 1,4 ou 1/2 do tempo da versão antiga) dentro de um um ou dois dias mexendo nos sinalizadores do compilador. Concedido, isso pode ser um comentário sobre a falta de conhecimento do compilador entre os cientistas que originaram grande parte do código em que trabalho, ao invés de um sintoma de minha excelência. Tendo definido os sinalizadores do compilador para o máximo (e raramente é apenas -O3), pode levar meses de trabalho duro para obter outro fator de 1,05 ou 1,1

Marca de alto desempenho
fonte
2

Quando o DEC saiu com seus processadores alfa, havia uma recomendação para manter o número de argumentos para uma função abaixo de 7, já que o compilador sempre tentaria colocar até 6 argumentos nos registradores automaticamente.

EvilTeach
fonte
O bit x86-64 também permite muitos parâmetros passados ​​pelo registro, o que pode ter um efeito dramático na sobrecarga da chamada de função.
Tom,
1

Para desempenho, concentre-se primeiro em escrever código de manutenção - componentizado, fracamente acoplado, etc, então quando você tiver que isolar uma parte para reescrever, otimizar ou simplesmente criar um perfil, você pode fazer isso sem muito esforço.

O otimizador ajudará marginalmente o desempenho do seu programa.

Ariel
fonte
3
Isso só funciona se as próprias "interfaces" de acoplamento forem passíveis de otimização. Uma interface pode ser inerentemente "lenta", por exemplo, forçando pesquisas ou cálculos redundantes ou forçando um acesso incorreto ao cache.
Tom,
1

Você está recebendo boas respostas aqui, mas eles presumem que seu programa está muito perto do ideal para começar, e você diz

Suponha que o programa foi escrito corretamente, compilado com otimização total, testado e colocado em produção.

Na minha experiência, um programa pode ser escrito corretamente, mas isso não significa que seja próximo do ideal. É preciso trabalho extra para chegar a esse ponto.

Se eu puder dar um exemplo, esta resposta mostra como um programa de aparência perfeitamente razoável foi feito 40 vezes mais rápido por macro-otimização . Grandes acelerações não podem ser feitas em todos os programas inicialmente escritos, mas em muitos (exceto para programas muito pequenos), pode, na minha experiência.

Depois que isso for feito, a micro-otimização (dos pontos críticos) pode dar uma boa recompensa.

Mike Dunlavey
fonte
1

eu uso o compilador Intel. no Windows e no Linux.

quando mais ou menos pronto, crio o perfil do código. em seguida, aguarde os pontos de acesso e tente alterar o código para permitir que o compilador faça um trabalho melhor.

se um código é computacional e contém muitos loops - o relatório de vetorização no compilador Intel é muito útil - procure por 'vec-report' na ajuda.

portanto, a ideia principal - polir o código crítico de desempenho. quanto ao resto - prioridade a ser correta e sustentável - funções curtas, código claro que poderia ser entendido 1 ano depois.

jf.
fonte
Você está quase respondendo à pergunta ... que tipo de coisas você faz com o código, para possibilitar que o compilador faça esses tipos de otimizações?
EvilTeach
1
Tentando escrever mais no estilo C (vs. em C ++), por exemplo, evitando funções virtuais sem necessidade absoluta, especialmente se elas forem chamadas frequentemente, evite AddRefs ... e todas as coisas legais (novamente, a menos que seja realmente necessário). Escrever código fácil para embutir - menos parâmetros, menos "se" -s. Não use variáveis ​​globais, a menos que seja absolutamente necessário. Na estrutura de dados - coloque os campos mais largos primeiro (double, int64 vai antes de int) - para que o compilador alinhe a estrutura no tamanho natural do primeiro campo - o alinhamento é bom para o perf.
jf.
1
O layout e o acesso aos dados são absolutamente essenciais para o desempenho. Então, após a criação de perfil - às vezes eu divido uma estrutura em várias, seguindo a localidade dos acessos. Mais um truque geral - use int ou size-t vs. char - mesmo os valores dos dados são pequenos - evite vários perf. penalidades armazenamento para bloqueio de carga, problemas com paradas de registros parciais. é claro que isso não se aplica quando precisam de grandes matrizes de tais dados.
jf.
Mais um - evite chamadas de sistema, a menos que haja uma necessidade real :) - eles são MUITO caros
jf.
2
@jf: Marquei +1 em sua resposta, mas você poderia mover a resposta dos comentários para o corpo da resposta? Será mais fácil de ler.
Kriss,
1

Uma otimização que usei em C ++ é a criação de um construtor que não faz nada. Deve-se chamar manualmente um init () para colocar o objeto em um estado de funcionamento.

Isso traz benefícios no caso de eu precisar de um grande vetor dessas classes.

Chamo reserve () para alocar o espaço para o vetor, mas o construtor não toca na página de memória em que o objeto está. Portanto, gastei algum espaço de endereço, mas não consumi realmente muita memória física. Eu evito as falhas de página associadas aos custos de construção associados.

Conforme eu gero objetos para preencher o vetor, eu os defino usando init (). Isso limita o total de falhas de página e evita a necessidade de redimensionar () o vetor ao preenchê-lo.

EvilTeach
fonte
6
Eu acredito que uma implementação típica de std :: vector não constrói mais objetos quando você reserva () mais capacidade. Ele apenas aloca páginas. Os construtores são chamados mais tarde, usando o novo posicionamento, quando você realmente adiciona objetos ao vetor - o que é (presumivelmente) logo antes de você chamar init (), então você realmente não precisa da função init () separada. Lembre-se também de que, mesmo que seu construtor esteja "vazio" no código-fonte, o construtor compilado pode conter código para inicializar coisas como tabelas virtuais e RTTI, de forma que as páginas sejam tocadas no momento da construção de qualquer maneira.
Wyzard
1
Sim. Em nosso caso, usamos push_back para preencher o vetor. Os objetos não têm funções virtuais, por isso não é um problema. A primeira vez que tentamos com o construtor, ficamos surpresos com o volume de falhas de página. Percebi o que aconteceu, arrancamos as entranhas do construtor e o problema de falta de página desapareceu.
EvilTeach
Isso me surpreende bastante. Quais implementações C ++ e STL você estava usando?
David Thornley,
3
Eu concordo com os outros, isso soa como uma implementação ruim de std :: vector. Mesmo se seus objetos tivessem vtables, eles não seriam construídos até seu push_back. Você deve ser capaz de testar isso declarando o construtor padrão como privado, porque todo o vetor de que precisará é o construtor de cópia para push_back.
Tom,
1
@David - A implementação foi em AIX.
EvilTeach
1

Uma coisa que fiz foi tentar manter as ações caras em lugares onde o usuário pode esperar que o programa demore um pouco. O desempenho geral está relacionado à capacidade de resposta, mas não é exatamente o mesmo e, para muitas coisas, a capacidade de resposta é a parte mais importante do desempenho.

A última vez que realmente tive que fazer melhorias no desempenho geral, fiquei atento a algoritmos subótimos e procurei locais que provavelmente apresentariam problemas de cache. Fiz o perfil e avaliei o desempenho primeiro e novamente após cada mudança. Então a empresa entrou em colapso, mas foi um trabalho interessante e instrutivo de qualquer maneira.

David Thornley
fonte
0

Há muito tempo eu suspeitava, mas nunca provei, que declarar matrizes de modo que tenham uma potência de 2, como o número de elementos, permite que o otimizador faça uma redução de força substituindo uma multiplicação por um deslocamento por um número de bits, ao olhar para cima elementos individuais.

2 rotações
fonte
6
Isso costumava ser verdade, hoje em dia é mais. De fato, exatamente o oposto é verdadeiro. Se você declarar seus arrays com potências de dois, muito provavelmente se deparará com a situação de trabalhar em dois ponteiros com potências de dois separados na memória. O problema é que os caches da CPU são organizados dessa forma e você pode acabar com os dois arrays lutando em torno de uma linha de cache. Você consegue um desempenho horrível dessa forma. Ter um dos ponteiros alguns bytes à frente (por exemplo, sem potência de dois) evita essa situação.
Nils Pipenbrinck
+1 Nils, e uma ocorrência específica disso é o "aliasing de 64k" no hardware Intel.
Tom,
Isso é algo que é facilmente refutado olhando para a desmontagem, aliás. Fiquei surpreso, anos atrás, ao ver como o gcc otimizaria todos os tipos de multiplicações constantes com mudanças e adições. Por exemplo, se val * 7transformou no que seria de outra forma (val << 3) - val.
dash-tom-bang
0

Coloque funções pequenas e / ou freqüentemente chamadas no topo do arquivo de origem. Isso torna mais fácil para o compilador encontrar oportunidades para inlining.

Mark Ransom
fonte
Realmente? Você pode citar uma justificativa e exemplos para isso? Não estou dizendo que não seja verdade, apenas não parece intuitivo que a localização seja importante.
sublinhado_d
@underscore_d não pode embutir algo até que a definição da função seja conhecida. Embora os compiladores modernos possam fazer várias passagens para que a definição seja conhecida no momento da geração do código, não presumo.
Mark Ransom
Eu presumi que os compiladores funcionam com gráficos de chamadas abstratos em vez de ordem de função física, o que significa que isso não importaria. Claro, suponho que não faz mal ser extremamente cuidadoso - especialmente quando, desempenho à parte, IMO, parece mais lógico definir funções que são chamadas antes daquelas que as chamam. Eu teria que testar o desempenho, mas ficaria surpreso se importasse, mas até então, estou aberto a ser surpreendido!
sublinhado_d