Em C ++, devo me preocupar em armazenar variáveis ​​em cache ou deixar que o compilador faça a otimização? (Aliasing)

114

Considere o seguinte código ( pé do tipo unsigned char*e bitmap->widthé de algum tipo inteiro, exatamente que é desconhecido e depende de qual versão de alguma biblioteca externa estamos usando):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Vale a pena otimizá-lo [..]

Pode haver um caso em que isso possa produzir resultados mais eficientes por escrito:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... ou isso é trivial para o compilador otimizar?

O que você considera um código "melhor"?

Nota do editor (Ike): para aqueles que estão se perguntando sobre o texto tachado, a pergunta original, conforme formulada, estava perigosamente perto de um território fora do tópico e estava muito perto de ser fechada, apesar do feedback positivo. Estes foram eliminados. No entanto, por favor, não puna os respondentes que abordaram essas seções contundentes da pergunta.

Yaron Cohen-Tal
fonte
19
Se *pfor do mesmo tipo que widthentão, não é trivial otimizar, pois ppode apontar widthe modificá-lo dentro do loop.
emlai
31
Perguntar se o compilador otimiza uma operação específica geralmente é a pergunta errada. Em última análise, você (normalmente) está interessado em saber qual versão roda mais rápido, o que você deve simplesmente medir.
SirGuy
4
@GuyGreer Eu concordo, embora eu diria que a pergunta é boa, ou pelo menos interessante, mas infelizmente a resposta é "você tem que medi-la, por caso de uso". A razão é que a funcionalidade é portátil, mas o desempenho não. Portanto, na verdade, depende de cada parte do processo de construção, começando no compilador e terminando no site de destino (combinação de sistema operacional / hardware). E, claro, o melhor palpite é que o compilador é mais inteligente do que o humano nisso.
luk32
19
Se eu fosse um compilador, veria que seus dois exemplos não são iguais. É possível que paponte para a mesma memória que bitmap->width. Portanto, não posso otimizar legalmente o primeiro exemplo para o segundo.
Mysticial
4
Onde "p" está armazenado? Eu sugeriria que você poderia obter uma vitória de desempenho realmente grande fazendo algo como "char * restringir p2 = p;" e, em seguida, usando "p2" em vez de "p" em seu loop. Então, se você quiser que as alterações em "p2" sejam aplicadas de volta a p, use "p + = (p2-p);" Observe que nenhum ponteiro escrito dentro do tempo de vida de p2 por um ponteiro não copiado de p2 pode ser lido usando um ponteiro copiado de p2, nem vice-versa, e nenhuma cópia de p2 pode ser usada para qualquer finalidade após o tempo de vida de p2, mas um compilador pode usar aqueles fatos para permitir otimizações que não podem ser realizadas por qualquer outro meio.
supercat

Respostas:

81

À primeira vista, pensei que o compilador poderia gerar assembly equivalente para ambas as versões com sinalizadores de otimização ativados. Quando verifiquei, fiquei surpreso ao ver o resultado:

Fonte unoptimized.cpp

nota: este código não deve ser executado.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Fonte optimized.cpp

nota: este código não deve ser executado.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Compilação

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Montagem (unoptimized.s)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Montagem (otimizado.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

diferença

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

O assembly gerado para a versão otimizada realmente carrega ( lea) a widthconstante, ao contrário da versão não otimizada que calcula o widthdeslocamento em cada iteração ( movq).

Quando eu tiver tempo, eventualmente postarei alguns benchmarks sobre isso. Boa pergunta.

YSC
fonte
3
Seria interessante ver se o código foi gerado de forma diferente se você lançar para, em const unsignedvez de apenas unsignedno caso não otimizado.
Mark Ransom
2
@MarkRansom Eu acho que não deve fazer diferença: A "promessa" de ser constante é apenas durante uma única comparação, não para todo o ciclo
Hagen von Eitzen
13
Por favor, NUNCA usar a função mainde teste para uma otimização. O Gcc o marca propositadamente como frio e, portanto, desabilita algumas otimizações para ele. Não sei se é esse o caso aqui, mas é um hábito importante para adquirir.
Marc Glisse
3
@MarcGlisse Você está 100% certo. Escrevi com pressa, vou melhorar isso.
YSC
3
Aqui está um link para ambas as funções em uma unidade de compilação em godbolt , assumindo que bitmapseja global. A versão não CSEd usa um operando de memória para cmp, o que não é um problema para o desempenho neste caso. Se fosse um local, o compilador poderia assumir que outros ponteiros não poderiam "saber" sobre ele e apontar para ele. Não é uma má ideia armazenar expressões envolvendo globais em variáveis ​​temporárias, desde que melhore (ou não prejudique) a legibilidade ou se o desempenho for crítico. A menos que haja muita coisa acontecendo, esses moradores geralmente podem apenas viver em registros e nunca ser derramados.
Peter Cordes
38

Na verdade, não há informações suficientes em seu snippet de código para ser capaz de dizer, e a única coisa que posso pensar é em aliasing. Do nosso ponto de vista, é bastante claro que você não quer pe bitmapapontar para o mesmo local na memória, mas o compilador não sabe disso e (porque pé do tipo char*) o compilador tem que fazer esse código funcionar mesmo que pebitmap sobrepõem.

Isso significa que, neste caso, se o loop muda bitmap->widthatravés do ponteiro, pentão isso deve ser visto ao relerbitmap->width mais tarde, o que por sua vez significa que armazená-lo em uma variável local seria ilegal.

Dito isso, acredito que alguns compiladores às vezes geram duas versões do mesmo código (vi evidências circunstanciais disso, mas nunca busquei informações diretamente sobre o que o compilador está fazendo neste caso) e verificam rapidamente se os ponteiros alias e executar o código mais rápido se determinar que está tudo bem.

Dito isso, mantenho meu comentário sobre simplesmente medir o desempenho das duas versões, meu dinheiro está em não ver qualquer diferença de desempenho consistente entre as duas versões do código.

Na minha opinião, perguntas como essas são aceitáveis ​​se seu objetivo é aprender sobre teorias e técnicas de otimização de compiladores, mas é uma perda de tempo (uma micro-otimização inútil) se seu objetivo final aqui é fazer o programa rodar mais rápido.

SirGuy
fonte
1
@GuyGreer: É um grande bloqueador de otimização; Considero lamentável que as regras de linguagem se concentrem em regras sobre tipos eficazes, em vez de identificar situações em que gravações e leituras de itens diferentes são ou não sem sequência. Regras escritas em tal termo poderiam atender melhor às necessidades do compilador e do programador do que as atuais.
supercat
3
@GuyGreer - um restrictqualificador não seria a resposta para o problema de aliasing neste caso?
LThode
4
Na minha experiência, restricté basicamente um acerto e erro. MSVC é o único compilador que vi que parece fazer isso corretamente. O ICC perde informações de aliasing por meio de chamadas de função, mesmo se estiverem embutidas. E o GCC geralmente falha em obter qualquer benefício, a menos que você declare cada parâmetro de entrada como restrict(incluindo thispara funções de membro).
Mysticial
1
@Mystical: Uma coisa a lembrar é que chartodos os tipos são apelidos, então se você tiver um char *, terá que usar restrictem tudo. Ou se você forçou as regras estritas de aliasing do GCC, -fno-strict-aliasingentão tudo é considerado um possível alias.
Zan Lynx
1
@Ray A proposta mais recente de restrictsemântica semelhante a em C ++ é o N4150 .
TC
24

Ok, pessoal, então eu medi, com GCC -O3(usando GCC 4.9 no Linux x64).

Acontece que a segunda versão é 54% mais rápida!

Então, eu acho que o aliasing é a coisa, eu não tinha pensado nisso.

[Editar]

Tentei novamente a primeira versão com todos os ponteiros definidos com __restrict__e os resultados são os mesmos. Estranho .. Ou o aliasing não é o problema ou, por algum motivo, o compilador não o otimiza bem, mesmo com __restrict__.

[Editar 2]

Ok, acho que consegui provar que o aliasing é o problema. Repeti meu teste original, desta vez usando uma matriz em vez de um ponteiro:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

E medido (teve que usar "-mcmodel = large" para vinculá-lo). Então eu tentei:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Os resultados da medida foram os mesmos - parece que o compilador foi capaz de otimizá-lo sozinho.

Então tentei os códigos originais (com um ponteiro p), desta vez quando pé do tipo std::uint16_t*. Novamente, os resultados foram os mesmos - devido ao aliasing estrito. Em seguida, tentei construir com "-fno-strict-aliasing" e novamente vi uma diferença no tempo.

Yaron Cohen-Tal
fonte
4
Parece que deveria ser um comentário, embora tecnicamente responda à pergunta. Observe também que, infelizmente, você não demonstrou que o aliasing era o problema. Parece provável, certamente plausível, mas isso é diferente de concluir que foi isso.
SirGuy
@GuyGreer: Veja meu [editar] - agora acho que está praticamente comprovado.
Yaron Cohen-Tal
2
Eu só me pergunto por que você começou a usar a variável "i" quando você tem "x" em seu loop?
Jesper Madsen
1
Sou eu que acha a frase 54% mais rápida difícil de compreender? Você quer dizer que é 1,54 vezes a velocidade do não otimizado, ou outra coisa?
Roddy
3
@ YaronCohen-Tal então duas vezes mais rápido? Impressionante, mas não o que eu teria entendido como "54% mais rápido"!
Roddy
24

Outras respostas apontaram que içar a operação do ponteiro para fora do loop pode alterar o comportamento definido devido às regras de aliasing que permitem que char faça alias de qualquer coisa e, portanto, não é uma otimização permitida para um compilador, embora na maioria dos casos seja obviamente correto para um humano programador.

Eles também apontaram que içar a operação para fora do loop é geralmente, mas nem sempre, uma melhoria do ponto de vista do desempenho e, muitas vezes, é negativo do ponto de vista da legibilidade.

Gostaria de salientar que muitas vezes existe uma "terceira via". Em vez de contar até o número de iterações desejado, você pode contar até zero. Isso significa que o número de iterações só é necessário uma vez no início do loop, não precisa ser armazenado depois disso. Melhor ainda, no nível do montador, muitas vezes elimina a necessidade de uma comparação explícita, pois a operação de decremento normalmente definirá sinalizadores que indicam se o contador era zero antes (sinalizador de transporte) e depois (sinalizador zero) do decréscimo.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Observe que esta versão do loop fornece valores de x na faixa 1 .. largura em vez de na faixa 0 .. (largura-1). Isso não importa no seu caso, porque você não está usando x para nada, mas é algo que você deve estar ciente. Se você quiser um ciclo de contagem regressiva com valores de x no intervalo 0 .. (largura-1), você pode fazer.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Você também pode se livrar dos casts nos exemplos acima se quiser, sem se preocupar com o impacto nas regras de comparação, já que tudo o que você está fazendo com bitmap-> largura é atribuí-lo diretamente a uma variável.

plugwash
fonte
2
Eu vi aquele segundo caso formatado como x --> 0, resultando no operador "downto". Muito engraçado. PS Não considero fazer uma variável para a condição final ser negativa para legibilidade, na verdade pode ser o oposto.
Mark Ransom
Realmente depende, às vezes uma instrução fica tão horrível que dividi-la em várias afirmações melhora a legibilidade, mas não acredito que seja o caso aqui.
plugwash de
1
1 Boa observação, embora eu argumentasse que içar static_cast<unsigned>(bitmap->width)e usar widthno loop é na verdade uma melhoria para legibilidade porque agora há menos coisas para o leitor analisar por linha. No entanto, as opiniões de outros podem ser diferentes.
SirGuy
1
Existem muitas outras situações em que a contagem regressiva é superior (por exemplo, ao remover itens de uma lista). Não sei por que isso não é feito com mais frequência.
Ian Goldby
3
Se você quiser escrever loops que se parecem mais com o conjunto ideal, use do { } while(), porque no ASM você faz loops com uma ramificação condicional no final. Os loops for(){}e usuais while(){}requerem instruções extras para testar a condição do loop uma vez antes do loop, se o compilador não puder provar que sempre é executado pelo menos uma vez. De qualquer forma, use for()ou while()quando for útil para verificar se o loop deve ser executado uma vez ou quando está mais legível.
Peter Cordes
11

A única coisa aqui que pode impedir a otimização é a regra de aliasing estrita . Resumindo :

"Aliasing estrito é uma suposição, feita pelo compilador C (ou C ++), de que ponteiros de desreferenciamento para objetos de tipos diferentes nunca se referirão ao mesmo local de memória (ou seja, alias uns aos outros)."

[…]

A exceção à regra é a char*, que pode apontar para qualquer tipo.

A exceção também se aplica a unsignedesigned char ponteiros.

Este é o caso do seu código: você está modificando *ppor meio de pqual é um unsigned char*, portanto, o compilador deve presumir que ele pode apontar para bitmap->width. Portanto, o armazenamento em cache de bitmap->widthé uma otimização inválida. Este comportamento de prevenção de otimização é mostrado na resposta de YSC .

Se e somente se papontado para um não char- decltype(bitmap->width)tipo e não- tipo, o cache seria uma otimização possível.

emlai
fonte
10

A pergunta feita originalmente:

Vale a pena otimizá-lo?

E minha resposta a isso (obtendo uma boa combinação de votos positivos e negativos ...)

Deixe o compilador se preocupar com isso.

O compilador quase certamente fará um trabalho melhor do que você. E não há garantia de que sua 'otimização' seja melhor do que o código 'óbvio' - você mediu ??

Mais importante, você tem alguma prova de que o código que está otimizando tem algum impacto no desempenho de seu programa?

Apesar dos votos negativos (e agora vendo o problema do aliasing), ainda estou feliz com essa resposta válida. Se você não sabe se vale a pena otimizar algo, provavelmente não é.

Uma questão bem diferente, é claro, seria esta:

Como posso saber se vale a pena otimizar um fragmento de código?

Primeiro, seu aplicativo ou biblioteca precisa ser executado mais rápido do que atualmente? O usuário ficou esperando muito tempo? O seu software prevê o tempo de ontem em vez de amanhã?

Só você pode realmente dizer isso, com base na finalidade do seu software e nas expectativas dos usuários.

Supondo que seu software precise de alguma otimização, a próxima coisa a fazer é começar a medir. Os criadores de perfil dirão a você onde seu código gasta o tempo. Se o seu fragmento não estiver sendo mostrado como um gargalo, é melhor deixá-lo sozinho. Perfiladores e outras ferramentas de medição também dirão se suas alterações fizeram a diferença. É possível passar horas tentando otimizar o código, apenas para descobrir que você não fez nenhuma diferença perceptível.

O que você quer dizer com 'otimizar', afinal?

Se você não está escrevendo código 'otimizado', então seu código deve ser tão claro, limpo e conciso quanto possível. O argumento "Otimização prematura é má" não é desculpa para código desleixado ou ineficiente.

O código otimizado normalmente sacrifica alguns dos atributos acima para desempenho. Pode envolver a introdução de variáveis ​​locais adicionais, tendo objetos com escopo mais amplo do que o esperado ou até mesmo a reversão da ordem normal do loop. Tudo isso pode ser menos claro ou conciso, então documente o código (resumidamente!) Sobre por que você está fazendo isso.

Mas frequentemente, com código 'lento', essas micro-otimizações são o último recurso. O primeiro lugar a olhar é para algoritmos e estruturas de dados. Existe alguma maneira de evitar fazer o trabalho? As pesquisas lineares podem ser substituídas por binárias? Uma lista vinculada seria mais rápida aqui do que um vetor? Ou uma mesa de hash? Posso armazenar os resultados em cache? Tomar boas decisões 'eficientes' aqui muitas vezes pode afetar o desempenho em uma ordem de magnitude ou mais!

Roddy
fonte
12
Quando você está iterando na largura de uma imagem de bitmap, a lógica do loop pode ser uma parte significativa do tempo gasto no loop. Em vez de se preocupar com a otimização prematura, é melhor, neste caso, desenvolver práticas recomendadas que sejam eficientes desde o início.
Mark Ransom
4
@MarkRansom concordou, em parte: Mas as "melhores práticas" seriam a: use uma biblioteca existente ou chamada de API para preencher imagens, ou b: faça com que a GPU faça isso por você. Nunca deve ser o tipo de micro-otimização não medida que o OP sugere. E como você sabe que este código é executado mais de uma vez, ou com bitmaps maiores que 16 pixels de largura ...?
Roddy
@Veedrac. Agradeço a justificativa para o -1. O impulso da pergunta mudou sutil e substancialmente desde que respondi. Se você acha que a resposta (expandida) ainda não ajuda, é hora de excluí-la ... "Vale a pena ..." é sempre baseado principalmente em opiniões, de qualquer maneira.
Roddy
@Roddy Agradeço as edições, elas ajudam (e meu comentário provavelmente soou muito duro de qualquer maneira). Ainda estou em cima do muro, já que essa é realmente uma resposta a uma pergunta que não é adequada para Stack Overflow. Parece que uma resposta adequada seria específica para o snippet, como são as respostas mais votadas aqui.
Veedrac
6

Eu uso o seguinte padrão em situações como esta. É quase tão curto quanto o primeiro caso seu, e é melhor do que o segundo caso, porque mantém a variável temporária local para o loop.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Isso será mais rápido com um compilador menos inteligente, build de depuração ou certos sinalizadores de compilação.

Edit1 : Colocar uma operação constante fora de um loop é uma boa padrão de programação. Mostra a compreensão dos fundamentos da operação da máquina, especialmente em C / C ++. Eu diria que o esforço para se provar deve ser feito em pessoas que não seguem essa prática. Se o compilador punir por um bom padrão, é um bug no compilador.

Edit2:: Eu medi minha sugestão contra o código original no vs2013, obtive% 1 melhoria. Podemos fazer melhor? Uma otimização manual simples oferece 3 vezes uma melhoria em relação ao loop original na máquina x64, sem recorrer a instruções exóticas. O código a seguir assume o sistema little endian e um bitmap alinhado corretamente. O TESTE 0 é original (9 segundos), o TESTE 1 é mais rápido (3 segundos). Aposto que alguém poderia tornar isso ainda mais rápido, e o resultado do teste dependeria do tamanho do bitmap. Definitivamente, em breve, no futuro, o compilador será capaz de produzir código consistentemente mais rápido. Temo que este seja o futuro, quando o compilador também for um programador AI, então estaríamos sem trabalho. Mas, por enquanto, apenas escreva um código que mostre que você sabe que não é necessária uma operação extra no loop.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}
0kcats
fonte
Você pode economizar outros 25% em 64 bits se usar três int64_t em vez de int64_t e int32_t.
Antonín Lejsek de
5

Existem duas coisas a serem consideradas.

A) Com que frequência a otimização será executada?

Se a resposta não for muito frequente, como apenas quando um usuário clica em um botão, não se preocupe se isso tornar seu código ilegível. Se a resposta for 1000 vezes por segundo, provavelmente você desejará ir com a otimização. Se for um pouco complexo, certifique-se de colocar um comentário explicando o que está acontecendo para ajudar o próximo cara que aparecer.

B) Isso tornará o código mais difícil de manter / solucionar problemas?

Se você não está vendo um grande ganho no desempenho, tornar seu código enigmático simplesmente para economizar alguns tique-taques não é uma boa ideia. Muitas pessoas dirão que qualquer bom programador deve ser capaz de olhar o código e descobrir o que está acontecendo. Isso é verdade. O problema é que, no mundo dos negócios, o tempo extra para descobrir isso custa dinheiro. Portanto, se você pode torná-lo mais bonito de ler, faça-o. Seus amigos vão agradecer por isso.

Dito isso, eu usaria pessoalmente o exemplo B.

soulsabr
fonte
4

O compilador é capaz de otimizar muitas coisas. Para seu exemplo, você deve ir para a legibilidade, manutenção e o que segue o seu padrão de código. Para obter mais informações sobre o que pode ser otimizado (com o GCC), consulte esta postagem do blog .

Guillaume Racicot
fonte
4

Como regra geral, deixe o compilador fazer a otimização para você, até que você determine que deve assumir. A lógica para isso não tem nada a ver com desempenho, mas sim com legibilidade humana. No vasto maioria dos casos, a legibilidade de seu programa é mais importante do que seu desempenho. Você deve ter como objetivo escrever um código que seja mais fácil de ser lido por um ser humano e só se preocupar com a otimização quando estiver convencido de que o desempenho é mais importante do que a manutenção do código.

Depois de ver que o desempenho é importante, você deve executar um criador de perfil no código para determinar quais loops estão sendo ineficientes e otimizá-los individualmente. De fato, pode haver casos em que você deseja fazer essa otimização (especialmente se você migrar para C ++, onde os contêineres STL são envolvidos), mas o custo em termos de legibilidade é grande.

Além disso, posso pensar em situações patológicas em que isso poderia realmente desacelerar o código. Por exemplo, considere o caso em que o compilador não conseguiu provar que bitmap->widthera constante durante o processo. Ao adicionar a widthvariável, você força o compilador a manter uma variável local nesse escopo. Se, por algum motivo específico da plataforma, essa variável extra impediu alguma otimização do espaço de pilha, pode ser necessário reorganizar como está emitindo bytecodes e produzir algo menos eficiente.

A título de exemplo, no Windows x64, é obrigado a chamar uma chamada API especial, __chkstkno preâmbulo da função, se a função for usar mais de 1 página de variáveis ​​locais. Esta função dá às janelas a chance de gerenciar as páginas de proteção que usam para expandir a pilha quando necessário. Se sua variável extra aumenta o uso da pilha de menos de 1 página para mais ou menos de 1 página, sua função agora é obrigada a chamar __chkstktoda vez que for inserida. Se você quisesse otimizar esse loop em um caminho lento, na verdade poderia desacelerar o caminho rápido mais do que economizou no caminho lento!

Claro, é um pouco patológico, mas o ponto desse exemplo é que você pode realmente desacelerar o compilador. Isso apenas mostra que você precisa definir o perfil de seu trabalho para determinar para onde vão as otimizações. Enquanto isso, não sacrifique a legibilidade de forma alguma por uma otimização que pode ou não ser importante.

Cort Ammon
fonte
4
Eu gostaria que C e C ++ fornecessem mais maneiras de identificar explicitamente coisas com as quais o programador não se preocupa. Eles não apenas forneceriam mais chances para os compiladores otimizarem as coisas, mas também evitariam que outros programadores que leiam o código tivessem que adivinhar se, por exemplo, ele pode estar verificando novamente o bitmap-> largura todas as vezes para garantir que as alterações nele afetem o loop, ou se ele pode estar armazenando em cache bitmap-> largura para garantir que as alterações nele não afetem o loop. Ter um meio de dizer "Cache isto ou não - não me importo" deixaria claro o motivo da escolha do programador.
supercat
@supercat Concordo plenamente, como se pode ver se olharmos para as pilhas de línguas falhadas que tentei escrever para resolver isso. Descobri que é extremamente difícil definir "o que" alguém não se preocupa sem tanta sintaxe ímpia que simplesmente não vale a pena. Eu continuo minha busca em vão.
Cort Ammon
Não é possível defini-lo em todos os casos, mas acho que há muitos casos em que o sistema de tipos pode ajudar. É muito C decidiu fazer tipos de caracteres o "acessor universal" ao invés de ter um qualificador de tipo um pouco mais flexível do que "volátil" que poderia ser aplicado a qualquer tipo, com a semântica de que acessos de tais tipos seriam processados ​​em sequência com acessos do tipo equivalente não qualificado e também com acessos de todos os tipos de variáveis ​​com esse mesmo qualificador. Isso ajudaria a deixar claro se alguém estava usando tipos de caracteres porque precisava do ...
supercat
... comportamento falso, ou se alguém os estava usando porque eram do tamanho certo para atender às suas necessidades. Também seria útil ter barreiras de aliasing explícitas que poderiam, em muitos casos, ser colocadas fora dos loops, ao contrário das barreiras implícitas associadas aos acessos do tipo caractere.
supercat
1
Esta é uma conversa sábia, mas, geralmente, se você já selecionou C para sua tarefa, provavelmente o desempenho é muito importante e as diferentes regras devem ser aplicadas. Caso contrário, pode ser melhor usar Ruby, Java, Python ou algo semelhante.
Audrius Meskauskas
4

A comparação está errada, pois os dois trechos de código

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

e

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

não são equivalentes

No primeiro caso, widthé dependente e não const, e não se pode presumir que não pode mudar entre as iterações subsequentes. Portanto, não pode ser otimizado, mas deve ser verificado em cada loop .

No seu caso otimizado, uma variável local recebe o valor de bitmap->widthem algum ponto durante a execução do programa. O compilador pode verificar se isso realmente não muda.

Você pensou em multiencadeamento ou talvez o valor possa ser dependente externamente, de forma que seu valor seja volátil. Como se esperaria que o compilador descobrisse todas essas coisas se você não contasse?

O compilador só pode fazer o melhor que seu código permitir.

g24l
fonte
2

A menos que você saiba exatamente como o compilador otimiza o código, é melhor fazer suas próprias otimizações, mantendo a legibilidade e o design do código. Praticamente é difícil verificar o código assembly para cada função que escrevemos para novas versões do compilador.

Vinayak SM
fonte
1

O compilador não pode otimizar bitmap->widthporque o valor de widthpode ser alterado entre as iterações. Existem alguns motivos mais comuns:

  1. Multi-threading. O compilador não pode prever se outro encadeamento está prestes a mudar o valor.
  2. Modificação dentro do loop, às vezes não é simples dizer se a variável será alterada dentro do loop.
  3. É uma chamada de função, por exemplo, iterator::end()ou container::size()então é difícil prever se sempre retornará o mesmo resultado.

Resumindo (minha opinião pessoal) para locais que requerem alto nível de otimização você precisa fazer isso sozinho, em outros locais apenas deixe, o compilador pode otimizá-lo ou não, se não houver grande diferença a legibilidade do código é o alvo principal.

ST3
fonte