AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79597905
Accepted
Grigory Rechistov
Grigory Rechistov
Asked: 2025-04-29 16:13:47 +0800 CST2025-04-29 16:13:47 +0800 CST 2025-04-29 16:13:47 +0800 CST

Exemplo de um microbenchmark para demonstrar que o código embutido nem sempre é benéfico para o desempenho

  • 772

TL;DR: muitas fontes citam a afirmação de que o uso excessivo de funções em linha pode, às vezes, prejudicar o desempenho do aplicativo devido ao excesso de código ou outros fatores. Existe algum exemplo real de programa que demonstre isso de forma mensurável?


Lembre-se: a missão de um microbenchmark na vida é ampliar algum aspecto do desempenho do seu programa. Por isso, qualquer pessoa pode facilmente gerar um microbenchmark que faça qualquer problema parecer um grande problema. // Dicas de Desempenho de Rico Mariani

Muitos programadores com quem converso têm a noção de que o inline de funções é incondicionalmente benéfico para o desempenho do aplicativo. O código C/C++ que analiso frequentemente tem inlinepalavras-chave (ou equivalente) aplicadas gratuitamente a funções, independentemente de seu tamanho, finalidade, popularidade ou posicionamento.

Em muitos casos, esse hábito estranho (chamado aqui de "doença do inline" ) é inofensivo para o desempenho geral: os compiladores modernos têm bom senso sobre o que realmente deve ser incorporado, e muito pouco código é quente o suficiente para que o (não)inline faça alguma diferença. Ainda assim, muitas vezes é prejudicial ao design do software resultante: mais coisas acabam nos cabeçalhos, os arquivos não são mais compiláveis ​​independentemente, etc.

Embora seja bastante fácil demonstrar que a aplicação aleatória sem benchmarking contínuo não faz nenhuma diferença mensurável no desempenho final, estou procurando um exemplo extremo em que forçar a questão prejudica estritamente o desempenho.

Um microbenchmark será suficiente; embora não prove nada sobre os efeitos do inline em aplicações do mundo real, deve demonstrar de forma comprovada que aplicá-lo cegamente não é uma boa ideia incondicional . Essa é realmente a ideia por trás de quase qualquer processo de otimização de código: pode ajudar, pode prejudicar e, às vezes, não faz diferença.

Alguns requisitos para tal exemplo de microbenchmark.

  1. Deve ser um programa razoavelmente curto, de preferência em C ou C++; outras linguagens nas quais o inlining pode ser aplicado também são bem-vindas.
  2. Não precisa ser um programa fazendo algo útil, ele pode fazer coisas "bobas" só para carregar/estressar o hardware subjacente.
  3. Deve ser possível compilá-lo em dois modos: com inlining imposto e inlining desabilitado. Qualquer técnica para conseguir isso pode ser usada: compilação condicional para redefinir anotações de inlining, sinalizadores de backend do compilador para controlar o inlining, etc.
  4. Ele deve ser bem formado e exibir o mesmo comportamento bem definido, independentemente de em qual dos dois modos ele for compilado.
  5. Ele deve conter pelo menos duas funções, uma chamando a outra, com a intenção de afetar o inlining de pelo menos uma delas.
  6. Pode conter qualquer técnica para garantir/impor a inclusão de funções em linha. Por exemplo, inlineextensões padrão de palavras-chave ou específicas do compilador ( __forceinline, __attribute__ ((always_inline))etc.) podem ser usadas para instruir o compilador a fazê-lo, independentemente de seu julgamento.
  7. Ao executar, o desempenho (latência, tempo de execução ou métrica semelhante) pode ser facilmente reportado. Pode ser apenas usando time a.out, ou chamadas internas para um recurso de temporização em torno do código afetado.
  8. Por fim, quando compilado por pelo menos um compilador específico de uma versão específica e executado em pelo menos um sistema de destino, as duas variantes resultantes do programa exibem diferenças estatisticamente significativas, e a compilação forçada em linha é mais lenta do que a compilação não em linha .

Eu percebo que o desempenho depende muito dos parâmetros do host; o que é mais lento em uma máquina pode se tornar tão rápido quanto ou mais rápido em outra. Mas estou buscando o pior cenário, quando o inlining irrestrito for comprovadamente contraproducente.

O ideal é que outras opções de backend do compilador que não afetam o inlining (como nível geral de otimização etc.) sejam as mesmas para duas compilações, a fim de excluir a possibilidade de que a diferença observável seja explicada por elas e não pelo inlining aplicado/ignorado.


Tenho uma ideia de um ponto de partida para esse programa, mas preciso de mais ajuda para desenvolvê-lo:

  1. Uma função interna é grande o suficiente para quase não caber no cache de instruções da CPU.
  2. Uma função externa é grande o suficiente para que, se a função interna for incorporada à força, a seção de código resultante se torne maior que o cache de instruções da CPU.
  3. O fluxo de controle do programa é organizado de tal forma que, quando tudo é incorporado, ele sofre uma frequência maior de falhas de cache de instruções, liberações de cache ou eventos semelhantes que não aconteceriam se o embutimento não fosse imposto.
performance
  • 1 1 respostas
  • 43 Views

1 respostas

  • Voted
  1. Best Answer
    Sam Mason
    2025-04-29T23:43:24+08:002025-04-29T23:43:24+08:00

    Eu experimentei e usei o desenrolamento de loop do GCC para obter muito código de máquina a partir do seguinte código C:

    #include <stdint.h>
    
    // from https://prng.di.unimi.it/xoshiro256starstar.c
    static inline uint64_t rotl(const uint64_t x, int k) {
        return (x << k) | (x >> (64 - k));
    }
    static uint64_t s[4];
    // a fast PRNG that will get inlined to generate lots of code
    static uint64_t next(void) {
        const uint64_t result = rotl(s[1] * 5, 7) * 9;
    
        const uint64_t t = s[1] << 17;
    
        s[2] ^= s[0];
        s[3] ^= s[1];
        s[1] ^= s[2];
        s[0] ^= s[3];
    
        s[2] ^= t;
    
        s[3] = rotl(s[3], 45);
    
        return result;
    }
    
    uint64_t benchmark() {
      uint64_t sum = 0;
    #ifdef UNROLL
      #pragma GCC unroll 65534
    #endif
      for (int i = 0; i < 5000; i++) {
    // do something
        if (sum & 1) {
          sum += next() >> 60;
        } else {
          sum += 1;
        }
      }
    
      return sum;
    }
    
    #include <inttypes.h>
    #include <stdio.h>
    #include <sys/random.h>
    #include <time.h>
    
    int main() {
      struct timespec t0, t1;
    
      // initialise from urandom
      getrandom(s, sizeof(s), 0);
    
      // run test 5 times
      for (int i = 0; i < 5; i++) {
        clock_gettime(CLOCK_MONOTONIC, &t0);
    
        uint64_t res = benchmark();
    
        clock_gettime(CLOCK_MONOTONIC, &t1);
    
        double dt = t1.tv_sec - t0.tv_sec;
        dt += (t1.tv_nsec - t0.tv_nsec) / 1e9;
    
        printf("took %.1f us to calculate %" PRIu64 "\n", dt * 1e6, res);
      }
    }
    

    Salvando isso inlinecost.ce compilando via:

    gcc -Wall -Os -o inlinecost inlinecost.c
    gcc -Wall -Os -o inlinecost-unrolled inlinecost.c -DUNROLL
    

    dando-me os seguintes binários:

    -rwxr-xr-x 1 smason smason  15656 Apr 29 16:30 inlinecost
    -rwxr-xr-x 1 smason smason 597288 Apr 29 16:30 inlinecost-unrolled
    

    Mostrando que certamente está gerando mais código.

    Correr inlinecostme dá:

    took 12.4 us to calculate 26605
    took 12.0 us to calculate 26265
    took 12.3 us to calculate 26759
    took 12.1 us to calculate 26487
    took 12.3 us to calculate 26499
    

    enquanto inlinecost-unrolledme dá:

    took 167.4 us to calculate 27161
    took 28.1 us to calculate 26685
    took 24.8 us to calculate 26297
    took 25.0 us to calculate 26388
    took 24.2 us to calculate 26763
    

    Você pode ver que o código não embutido é executado de forma muito mais consistente, enquanto a versão desenrolada leva 10 vezes mais tempo para carregar o código de máquina da RAM para o cache e executá-lo, e então "apenas" leva o dobro do tempo para executá-lo.

    Ter o loop benchmarkgerando mais iterações (por exemplo, aumentando 5000 para 10000) torna essa diferença ainda mais visível, mas leva muito tempo para compilar.

    Aqui está um link para o GodBolt com apenas 5 iterações desenroladas (muitas iterações fazem com que a compilação atinja o tempo limite porque está gerando muito código), mostrando que ele está incorporando o PRNG.

    Espero que seja útil!

    Atualização: tentei mudar benchmarkpara fazer:

    uint64_t benchmark() {
      uint64_t sum = next();
    #ifdef UNROLL
    #pragma GCC unroll 65534
    #endif
      for (int i = 0; i < 30000; i++) {
        if (sum == 0) {
          uint64_t x = 0;
    #ifdef UNROLL
    #pragma GCC unroll 1024
    #endif
          for (int j = 0; j < 4; j++) {
            x ^= next();
          }
          sum += x >> 60;
        } else {
          sum += 1;
        }
      }
    
      return sum;
    }
    

    Isso leva a versão desenrolada a ~400µs na primeira vez, depois ~50µs nas iterações subsequentes, enquanto a versão em loop parece levar de forma confiável ~7µs. Eu esperava que o preditor de ramificação tivesse dificuldades com tanto código, mas pelo menos minha CPU está se saindo notavelmente bem com isso — um AMD 9900X, ou seja, Zen5. Não sei por que me lembrei do Zen4 no meu comentário abaixo.

    • 1

relate perguntas

  • Melhorando a eficiência no cálculo dos números Stirling

  • Problema de velocidade de Haskell onde a execução de ambas as partes de um programa leva significativamente mais tempo do que qualquer parte sozinha

  • Por que o código do script g otimizado é mais lento que o "ineficiente"?

  • Como fazer com que o usuário JMeter não espere resposta

  • Como é possível explicar a grande diferença de velocidade de execução entre dois processadores?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve