[...] (concedido, no ambiente de microssegundos) [...]
Os microssegundos se acumulam se ultrapassarmos milhões a bilhões de coisas. Uma sessão vtune / micro-otimização pessoal do C ++ (sem melhorias algorítmicas):
T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds
Tudo além de "multithreading", "SIMD" (escrito à mão para vencer o compilador) e a otimização de patches de 4 valências eram otimizações de memória em nível micro. Além disso, o código original a partir dos tempos iniciais de 32 segundos já foi bastante otimizado (complexidade algorítmica teoricamente ideal) e esta é uma sessão recente. A versão original muito antes desta sessão recente levou mais de 5 minutos para ser processada.
A otimização da eficiência da memória pode ajudar muitas vezes de várias vezes a ordens de magnitudes em um contexto de thread único e mais em contextos multithread (os benefícios de um representante de memória eficiente geralmente se multiplicam com vários threads no mix).
Sobre a importância da micro-otimização
Fico um pouco agitado com essa ideia de que as micro-otimizações são uma perda de tempo. Concordo que é um bom conselho geral, mas nem todos o fazem incorretamente com base em palpites e superstições, e não em medições. Feito corretamente, não produz necessariamente um micro impacto. Se pegarmos o próprio Embree (núcleo de raytracing) da Intel e testarmos apenas o BVH escalar simples que eles escreveram (não o pacote de raios que é exponencialmente mais difícil de vencer) e tentarmos superar o desempenho dessa estrutura de dados, pode ser o mais experiência humilhante mesmo para um veterano acostumado a criar perfis e ajustar códigos por décadas. E é tudo por causa das micro otimizações aplicadas. A solução deles pode processar mais de cem milhões de raios por segundo quando vi profissionais industriais trabalhando no rastreamento de raios que podem '
Não há como adotar uma implementação direta de um BVH com apenas um foco algorítmico e obter mais de cem milhões de interseções de raios primários por segundo contra qualquer compilador otimizador (mesmo o próprio ICC da Intel). Um simples nem sempre recebe um milhão de raios por segundo. É preciso soluções de qualidade profissional para obter, com frequência, alguns milhões de raios por segundo. É preciso uma micro otimização no nível da Intel para obter mais de cem milhões de raios por segundo.
Algoritmos
Eu acho que a micro-otimização não é importante, desde que o desempenho não seja importante no nível de minutos a segundos, por exemplo, ou de horas a minutos. Se pegarmos um algoritmo horrível, como a classificação por bolhas, e usá-lo sobre uma entrada em massa como exemplo, e depois compará-lo com uma implementação básica da classificação por mesclagem, a primeira pode levar meses para ser processada, e a última, talvez 12 minutos, como resultado de complexidade quadrática versus linearitmica.
A diferença entre meses e minutos provavelmente fará com que a maioria das pessoas, mesmo aquelas que não trabalham em campos críticos de desempenho, considere o tempo de execução inaceitável se exigir que os usuários esperem meses para obter um resultado.
Enquanto isso, se compararmos a classificação de mesclagem direta não micro otimizada com a classificação rápida (que não é de todo o algoritmo superior à classificação por mesclagem e oferece apenas melhorias em nível micro para a localidade de referência), a classificação rápida micro otimizada pode terminar em 15 segundos em oposição a 12 minutos. Fazer com que os usuários esperem 12 minutos pode ser perfeitamente aceitável (horário da pausa para o café).
Eu acho que essa diferença é provavelmente insignificante para a maioria das pessoas entre, digamos, 12 minutos e 15 segundos, e é por isso que a micro-otimização é frequentemente considerada inútil, pois geralmente é apenas a diferença entre minutos e segundos, e não minutos e meses. A outra razão pela qual acho que é inútil é que muitas vezes é aplicada a áreas que não importam: alguma pequena área que nem é louca e crítica, que produz uma diferença questionável de 1% (que pode muito bem ser apenas ruído). Mas para as pessoas que se preocupam com esse tipo de diferença de tempo e estão dispostas a medir e fazer o que é certo, acho que vale a pena prestar atenção pelo menos aos conceitos básicos da hierarquia de memória (especificamente os níveis superiores relacionados a falhas de página e falhas de cache) .
Java deixa muito espaço para boas micro-otimizações
Ufa, desculpe - com esse tipo de discurso de lado:
A "mágica" da JVM atrapalha a influência que um programador exerce sobre as micro-otimizações em Java?
Um pouco, mas não tanto quanto as pessoas possam pensar, se você fizer o que é certo. Por exemplo, se você estiver processando imagens, em código nativo com SIMD manuscrito, multithreading e otimizações de memória (padrões de acesso e possivelmente até representação dependendo do algoritmo de processamento de imagem), é fácil processar centenas de milhões de pixels por segundo por 32- pixels RGBA de bit (canais de cores de 8 bits) e às vezes até bilhões por segundo.
É impossível chegar perto em Java, se você disser, criou um Pixel
objeto (isso por si só aumentaria o tamanho de um pixel de 4 bytes para 16 em 64 bits).
Mas você poderá se aproximar muito mais se evitar o Pixel
objeto, usar uma matriz de bytes e modelar um Image
objeto. O Java ainda é bastante competente lá, se você começar a usar matrizes de dados antigos simples. Eu tentei esse tipo de coisa antes em Java e fiquei bastante impressionado, desde que você não crie um monte de pequenos objetos pequenininhos em todos os lugares que sejam 4 vezes maiores que o normal (ex: use em int
vez de Integer
) e comece a modelar interfaces em massa como uma Image
interface, não Pixel
interface. Atrevo-me a dizer que o Java pode rivalizar com o desempenho do C ++ se você estiver repetindo dados antigos simples e não objetos (grandes matrizes float
, por exemplo, não Float
).
Talvez ainda mais importante que o tamanho da memória seja que uma matriz int
garanta uma representação contígua. Uma matriz de Integer
não. A contiguidade geralmente é essencial para a localidade de referência, pois significa que vários elementos (ex: 16 ints
) podem caber em uma única linha de cache e potencialmente ser acessados juntos antes da remoção com padrões de acesso à memória eficientes. Enquanto isso, um único Integer
pode estar oculto em algum lugar da memória, sendo irrelevante a memória circundante, apenas para ter essa região de memória carregada em uma linha de cache apenas para usar um único número inteiro antes da remoção, em vez de 16 números inteiros. Mesmo se tivéssemos uma sorte maravilhosa e envolventeIntegers
estavam bem próximos um do outro na memória, só podemos encaixar 4 em uma linha de cache que pode ser acessada antes da remoção como resultado de Integer
ser quatro vezes maior, e esse é o melhor cenário.
E há muitas micro-otimizações disponíveis desde que estamos unificados sob a mesma arquitetura / hierarquia de memória. Os padrões de acesso à memória não importam qual linguagem você usa, conceitos como ladrilhos / bloqueios de loop geralmente podem ser aplicados com muito mais frequência em C ou C ++, mas eles beneficiam o Java da mesma forma.
Li recentemente em C ++, às vezes, a ordenação dos membros dos dados pode fornecer otimizações [...]
A ordem dos membros de dados geralmente não importa em Java, mas isso é principalmente uma coisa boa. Em C e C ++, preservar a ordem dos membros dos dados geralmente é importante por razões de ABI, para que os compiladores não mexam nisso. Os desenvolvedores humanos que trabalham lá precisam tomar cuidado para organizar coisas como os membros dos dados em ordem decrescente (maior para o menor) para evitar desperdiçar memória no preenchimento. Com o Java, aparentemente, o JIT pode reordenar os membros para você em tempo real para garantir o alinhamento adequado, minimizando o preenchimento, portanto, desde que seja o caso, ele automatiza algo que os programadores C e C ++ comuns podem fazer mal e acabam desperdiçando memória dessa maneira ( que não está apenas desperdiçando memória, mas muitas vezes desperdiçando velocidade, aumentando o passo entre as estruturas de AoS desnecessariamente e causando mais falhas de cache). Isto' É uma coisa muito robótica reorganizar os campos para minimizar o preenchimento; portanto, idealmente, os humanos não lidam com isso. O único momento em que o arranjo de campo pode ser importante para que um humano saiba o arranjo ideal é se o objeto for maior que 64 bytes e estivermos organizando campos com base no padrão de acesso (não no preenchimento ideal) - nesse caso pode ser um empreendimento mais humano (requer a compreensão de caminhos críticos, alguns dos quais são informações que um compilador não pode prever sem saber o que os usuários farão com o software).
Caso contrário, as pessoas poderiam dar exemplos de quais truques você pode usar em Java (além de simples sinalizadores de compilador).
A maior diferença para mim em termos de uma mentalidade otimizada entre Java e C ++ é que o C ++ pode permitir que você use objetos um pouco (pequenino) um pouco mais que o Java em um cenário crítico de desempenho. Por exemplo, o C ++ pode agrupar um número inteiro em uma classe sem sobrecarga (comparada em todo o lugar). O Java precisa ter essa sobrecarga de preenchimento de estilo de ponteiro de metadados + alinhamento por objeto, e é por isso que Boolean
é maior que boolean
(mas, em troca, fornece benefícios uniformes de reflexão e a capacidade de substituir qualquer função não marcada como final
para cada UDT).
É um pouco mais fácil em C ++ controlar a contiguidade dos layouts de memória em campos não homogêneos (ex: intercalar flutuações e ints em uma matriz por uma estrutura / classe), pois a localidade espacial geralmente é perdida (ou pelo menos o controle é perdido) em Java ao alocar objetos através do GC.
... mas geralmente as soluções de maior desempenho as dividem de qualquer maneira e usam um padrão de acesso SoA sobre matrizes contíguas de dados antigos simples. Portanto, para as áreas que precisam de desempenho máximo, as estratégias para otimizar o layout da memória entre Java e C ++ geralmente são as mesmas, e muitas vezes você precisa demolir essas pequenas interfaces orientadas a objetos em favor de interfaces no estilo de coleção que podem fazer coisas como hot / divisão de campo frio, representantes de SoA, etc. Representantes não homogêneos de AoSoA parecem meio impossíveis em Java (a menos que você tenha usado apenas uma matriz bruta de bytes ou algo parecido), mas esses são casos raros em que ambosos padrões de acesso seqüencial e aleatório precisam ser rápidos e, simultaneamente, ter uma mistura de tipos de campos para campos quentes. Para mim, a maior parte da diferença na estratégia de otimização (no tipo geral de nível) entre essas duas é discutível se você está buscando o desempenho máximo.
As diferenças variam um pouco mais se você simplesmente busca um desempenho "bom" - não é possível fazer o mesmo com objetos pequenos como Integer
vs. int
pode ser um pouco mais uma PITA, especialmente com a maneira como interage com genéricos . É um pouco mais difícil de apenas construir uma estrutura de dados genérico como um alvo de otimização central em Java que funciona para int
, float
, etc., evitando esses UDTs maiores e caros, mas muitas vezes as maioria das áreas de desempenho crítico vai exigir mão-rolando suas próprias estruturas de dados mesmo assim, é irritante para código que busca um bom desempenho, mas não um desempenho máximo.
Sobrecarga de objeto
Observe que a sobrecarga do objeto Java (metadados e perda da localidade espacial e perda temporária da localidade temporal após um ciclo inicial da GC) geralmente é grande para coisas realmente pequenas (como int
vs. Integer
) que estão sendo armazenadas aos milhões em alguma estrutura de dados que é amplamente contíguo e acessado em loops muito apertados. Parece haver muita sensibilidade sobre esse assunto, então devo esclarecer que você não quer se preocupar com sobrecarga de objetos para grandes objetos como imagens, apenas objetos minúsculos como um único pixel.
Se alguém tiver dúvidas sobre essa parte, sugiro fazer uma referência entre somar um milhão aleatório ints
versus um milhão aleatório Integers
e fazer isso repetidamente (o Integers
reorganizará na memória após um ciclo inicial de GC).
Ultimate Trick: Design de interface que deixa espaço para otimizar
Portanto, o truque final em Java, como eu vejo, se você estiver lidando com um local que lida com uma carga pesada sobre objetos pequenos (por exemplo: a Pixel
, um vetor de 4, uma matriz 4x4, um Particle
, possivelmente até um, Account
se tiver apenas alguns objetos pequenos campos) é evitar o uso de objetos para essas pequenas coisas e usar matrizes (possivelmente encadeadas) de dados antigos simples. Os objectos em seguida tornar-se interfaces de recolha como Image
, ParticleSystem
, Accounts
, um conjunto de matrizes ou vectores, etc. aqueles individuais podem ser acedidos pelo índice de, por exemplo, Este é também um dos truques de design finais em C e C ++, uma vez que mesmo sem que a sobrecarga objecto básico e memória desarticulada, modelar a interface no nível de uma única partícula impede as soluções mais eficientes.