Java é muito mais difícil de ajustar o desempenho em comparação com C / C ++? [fechadas]

11

A "mágica" da JVM atrapalha a influência que um programador exerce sobre as micro-otimizações em Java? Recentemente, li em C ++ algumas vezes que a ordem dos membros dos dados pode fornecer otimizações (concedidas, no ambiente de microssegundos) e presumi que as mãos de um programador estão atadas quando se trata de diminuir o desempenho do Java?

Aprecio que um algoritmo decente fornece maiores ganhos de velocidade, mas uma vez que você tenha o algoritmo correto, é mais difícil ajustar o Java devido ao controle da JVM?

Caso contrário, as pessoas poderiam dar exemplos de quais truques você pode usar em Java (além de simples sinalizadores de compilador).

user997112
fonte
14
O princípio básico por trás de toda otimização Java é este: A JVM provavelmente já fez isso melhor do que você pode. A otimização envolve principalmente seguir práticas de programação sensatas e evitar as coisas usuais, como concatenar seqüências de caracteres em um loop.
Robert Harvey
3
O princípio da micro-otimização em todas as línguas é que o compilador já fez isso melhor do que você pode. O outro princípio da micro-otimização em todas as línguas é que jogar mais hardware nele é mais barato que o tempo do programador na micro-otimização. O programador deve tender a dimensionar problemas (algoritmos abaixo do ideal), mas a micro-otimização é uma perda de tempo. Às vezes, a micro-otimização faz sentido em sistemas embarcados onde você não pode jogar mais hardware nele, mas o Android usando Java e uma implementação bastante ruim mostra que a maioria deles já possui hardware suficiente.
Jan Hudec
1
para "truques de desempenho Java", vale a pena estudar são: Effective Java , Angelika Langer Links - Java Desempenho e artigos relacionados com o desempenho de Brian Goetz em Java teoria e prática e de Threading Levemente séries listadas aqui
mosquito
2
Seja extremamente cuidadoso sobre dicas e truques - a JVM, sistemas operacionais e movimentos de hardware - você é o melhor fora de aprender a metodologia de ajuste de desempenho e aplicação de melhorias para o seu ambiente particular :-)
Martijn Verburg
Em alguns casos, uma VM pode fazer otimizações em tempo de execução impraticáveis ​​em tempo de compilação. O uso da memória gerenciada pode melhorar o desempenho, mas também costuma ter um maior espaço de memória. A memória não utilizada é liberada quando conveniente, e não o mais rápido possível.
22712 Brian As

Respostas:

5

Certamente, no nível de micro otimização, a JVM fará algumas coisas sobre as quais você terá pouco controle, em comparação com C e C ++, especialmente.

Por outro lado, a variedade de comportamentos do compilador com C e C ++ terá um impacto negativo muito maior na sua capacidade de realizar micro otimizações de qualquer tipo de maneira vagamente portátil (mesmo nas revisões do compilador).

Depende de que tipo de projeto você está aprimorando, quais ambientes você está direcionando e assim por diante. E, no final, isso realmente não importa, já que você está obtendo resultados melhores em algumas ordens de magnitude com otimizações de algoritmos / estrutura de dados / design de programas.

Telastyn
fonte
Pode importa muito quando você encontrar o seu aplicativo não escala através de núcleos
James
@james - gostaria de elaborar?
Telastyn #
1
@ James, o dimensionamento entre núcleos tem muito pouco a ver com a linguagem de implementação (exceto Python!), E mais a ver com a arquitetura de aplicativos.
James Anderson
29

As micro-otimizações quase nunca valem o tempo, e quase todas as fáceis são feitas automaticamente por compiladores e tempos de execução.

Há, no entanto, uma importante área de otimização em que C ++ e Java são fundamentalmente diferentes, e é o acesso à memória em massa. O C ++ possui gerenciamento manual de memória, o que significa que você pode otimizar o layout de dados e os padrões de acesso do aplicativo para fazer uso total de caches. Isso é bastante difícil, um pouco específico para o hardware em que você está executando (portanto, os ganhos de desempenho podem desaparecer em diferentes hardwares), mas, se bem feito, pode levar a um desempenho absolutamente impressionante. Claro que você paga por isso, com o potencial de todos os tipos de bugs horríveis.

Com uma linguagem de coleta de lixo como Java, esse tipo de otimização não pode ser feito no código. Alguns podem ser feitos pelo tempo de execução (automaticamente ou através da configuração, veja abaixo), e outros não são possíveis (o preço que você paga por estar protegido contra erros de gerenciamento de memória).

Caso contrário, as pessoas poderiam dar exemplos de quais truques você pode usar em Java (além de simples sinalizadores de compilador).

Os sinalizadores do compilador são irrelevantes em Java porque o compilador Java quase não faz otimização; o tempo de execução faz.

E, de fato, os tempos de execução do Java têm uma infinidade de parâmetros que podem ser ajustados, principalmente no que diz respeito ao coletor de lixo. Não há nada "simples" nessas opções - os padrões são bons para a maioria dos aplicativos e, para obter um melhor desempenho, você precisa entender exatamente o que as opções fazem e como o aplicativo se comporta.

Michael Borgwardt
fonte
1
+1: basicamente o que eu estava escrevendo na minha resposta, talvez uma formulação melhor.
Klaim
1
+1: pontos muito bons, explicados de uma maneira muito concisa: "Isso é bastante difícil ... mas, se bem feito, pode levar a um desempenho absolutamente deslumbrante. É claro que você paga por isso com o potencial de todos os tipos de bugs horríveis . "
Giorgio
1
@ MartinBa: É mais do que você paga para otimizar o gerenciamento de memória. Se você não tentar otimizar o gerenciamento de memória, o gerenciamento de memória C ++ não é tão difícil (evite-o inteiramente via STL ou torne-o relativamente fácil usando RAII). Obviamente, implementar RAII em C ++ requer mais linhas de código do que não fazer nada em Java (ou seja, porque Java lida com isso para você).
18712 Brian As
3
@ Martin Ba: Basicamente sim. Ponteiros oscilantes, estouros de buffer, ponteiros não inicializados, erros na aritmética dos ponteiros, tudo o que simplesmente não existe sem o gerenciamento manual de memória. E otimizar o acesso à memória exige bastante gerenciamento manual de memória.
Michael Borgwardt
1
Há algumas coisas que você pode fazer em java. Um deles é o pool de objetos, que maximiza as chances da localização dos objetos na memória (diferente do C ++, onde pode garantir a localização da memória).
RokL
5

[...] (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 Pixelobjeto (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 Pixelobjeto, usar uma matriz de bytes e modelar um Imageobjeto. 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 intvez de Integer) e comece a modelar interfaces em massa como uma Imageinterface, não Pixelinterface. 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 intgaranta uma representação contígua. Uma matriz de Integernã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 Integerpode 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 envolventeIntegersestavam 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 Integerser 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 finalpara 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 Integervs. intpode 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 intvs. 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 intsversus um milhão aleatório Integerse fazer isso repetidamente (o Integersreorganizará 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, Accountse 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.

ChrisF
fonte
1
Considerando que o desempenho ruim em massa pode realmente ter uma chance decente de superar o desempenho máximo nas áreas críticas, não acho que alguém possa desconsiderar completamente a vantagem de ter um bom desempenho com facilidade. E o truque de transformar uma matriz de estruturas em uma estrutura de matrizes se quebra um pouco quando todos os valores (ou quase todos) que compreendem uma das estruturas originais serão acessados ​​ao mesmo tempo. BTW: Vejo que você está desenterrando muitos posts velhote e adicionar sua própria resposta boa, às vezes até mesmo a boa resposta ;-)
Deduplicator
1
@Duplicador Espero que eu não esteja incomodando as pessoas batendo demais! Este ficou um pouco pequenino - talvez eu deva melhorar um pouco. SoA vs. AoS geralmente é difícil para mim (acesso seqüencial vs. acesso aleatório). Eu raramente sei de antemão qual deles devo usar, pois geralmente há uma mistura de acesso seqüencial e aleatório no meu caso. A lição valiosa que aprendi muitas vezes é projetar interfaces que deixam espaço suficiente para brincar com a representação de dados - interfaces meio mais volumosas que possuem grandes algoritmos de transformação quando possível (às vezes não é possível com pequenos bits acessados ​​aleatoriamente aqui e ali).
1
Bem, eu só notei porque as coisas são realmente lentas. E eu levei meu tempo com cada um.
Deduplicator
Eu realmente me pergunto por que user204677foi embora. Que ótima resposta.
Oligofren
3

Há uma área intermediária entre a micro-otimização, por um lado, e uma boa escolha de algoritmo, por outro.

É a área de acelerações de fator constante e pode produzir ordens de magnitude.
O modo como faz isso é cortando frações inteiras do tempo de execução, como 30%, 20% do que resta, 50% disso e outras várias iterações, até que quase não resta nada.

Você não vê isso em pequenos programas de demonstração. Onde você vê isso, está em grandes programas sérios, com muitas estruturas de dados de classe, em que a pilha de chamadas costuma ter muitas camadas de profundidade. Uma boa maneira de encontrar as oportunidades de aceleração é examinando amostras em tempo aleatório do estado do programa.

Geralmente, as acelerações consistem em coisas como:

  • minimizar chamadas para newagrupar e reutilizar objetos antigos,

  • reconhecer as coisas que estão sendo feitas que estão lá por uma questão de generalidade, em vez de serem realmente necessárias,

  • revisando a estrutura de dados usando diferentes classes de coleta que têm o mesmo comportamento big-O, mas aproveitam os padrões de acesso realmente usados,

  • salvar dados que foram adquiridos por chamadas de função em vez de chamar novamente a função (é uma tendência natural e divertida dos programadores assumir que funções com nomes mais curtos são executadas mais rapidamente).

  • tolerar uma certa inconsistência entre estruturas de dados redundantes, em vez de tentar mantê-las totalmente consistentes com eventos de notificação,

  • etc etc.

Mas é claro que nenhuma dessas coisas deve ser feita sem antes demonstrar problemas ao coletar amostras.

Mike Dunlavey
fonte
2

O Java (tanto quanto sei) não oferece controle sobre locais variáveis ​​na memória; portanto, é mais difícil evitar coisas como compartilhamento falso e alinhamento de variáveis ​​(você pode realizar uma aula com vários membros não utilizados). Outra coisa que eu acho que você não pode tirar proveito são instruções como mmpause, mas essas coisas são específicas da CPU e, portanto, se você precisar, o Java pode não ser a linguagem a ser usada.

Existe a classe Unsafe que oferece flexibilidade de C / C ++, mas também com o perigo de C / C ++.

Pode ajudá-lo a olhar para o código de montagem que a JVM gera para seu código

Para ler sobre um aplicativo Java que analisa esse tipo de detalhe, consulte o código Disruptor lançado pela LMAX

James
fonte
2

É muito difícil responder a essa pergunta, porque depende da implementação da linguagem.

Em geral, há muito pouco espaço para essas "micro otimizações" atualmente. O principal motivo é que os compiladores aproveitam essas otimizações durante a compilação. Por exemplo, não há diferença de desempenho entre operadores de pré-incremento e pós-incremento em situações em que suas semânticas são idênticas. Outro exemplo seria, por exemplo, um loop como este, no for(int i=0; i<vec.size(); i++)qual se poderia argumentar que, em vez de chamar osize()função membro durante cada iteração, seria melhor obter o tamanho do vetor antes do loop e então comparar com essa variável única e, assim, evitar a função de uma chamada por iteração. No entanto, há casos em que um compilador detectará esse caso bobo e armazenará em cache o resultado. No entanto, isso só é possível quando a função não tem efeitos colaterais e o compilador pode ter certeza de que o tamanho do vetor permanece constante durante o loop, por isso apenas se aplica a casos razoavelmente triviais.

zxcdw
fonte
Quanto ao segundo caso, não acho que o compilador possa otimizá-lo no futuro próximo. Detectar que é seguro otimizar vec.size () depende de provar que o tamanho se o vetor / perdido não muda dentro do loop, o que acredito ser indecidível devido ao problema de parada.
Lie Ryan
@LieRyan Eu já vi vários casos (simples) nos quais o compilador gerou arquivo binário exatamente idêntico se o resultado foi manualmente "armazenado em cache" e se size () tiver sido chamado. Eu escrevi um código e, ao que parece, o comportamento depende muito da maneira como o programa opera. Há casos em que o compilador pode garantir que não há possibilidade de o tamanho do vetor mudar durante o loop e há casos em que ele não pode garantir, muito parecido com o problema de interrupção, como você mencionou. Por agora eu sou incapaz de verificar o meu pedido (C ++ desmontagem é uma dor), então eu editei a resposta
zxcdw
2
@ Ryan Ryan: muitas coisas que são indecidíveis no caso geral são perfeitamente decidíveis para casos específicos, mas comuns, e isso é tudo o que você precisa aqui.
22812 Michael Borgwardt
@LieRyan Se você chamar apenas constmétodos nesse vetor, tenho certeza de que muitos compiladores de otimização descobrirão isso.
K.Steff
em C #, e acho que também li em Java, se você não armazenar em cache o tamanho, o compilador saberá que pode remover as verificações para verificar se você está fora dos limites da matriz e se o tamanho do cache deve fazer as verificações , que geralmente custam mais do que você está economizando em cache. Tentar enganar os otimizadores raramente é um bom plano.
Kate Gregory
1

as pessoas podem dar exemplos de quais truques você pode usar em Java (além de simples sinalizadores de compilador).

Além das melhorias nos algoritmos, certifique-se de considerar a hierarquia de memória e como o processador a utiliza. Existem grandes benefícios na redução das latências de acesso à memória, depois de entender como o idioma em questão aloca memória para seus tipos e objetos de dados.

Exemplo de Java para acessar uma matriz de 1000 x 1000 ints

Considere o código de exemplo abaixo - ele acessa a mesma área de memória (uma matriz de 1000x1000 de entradas), mas em uma ordem diferente. No meu mac mini (Core i7, 2,7 GHz), a saída é a seguinte, mostrando que percorrer o array por linhas mais do que duplica o desempenho (média acima de 100 rodadas cada).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Isso ocorre porque a matriz é armazenada de modo que colunas consecutivas (ou seja, valores int) sejam colocadas adjacentes na memória, enquanto linhas consecutivas não. Para o processador realmente usar os dados, ele precisa ser transferido para seus caches. A transferência de memória é feita por um bloco de bytes, chamado de linha de cache - carregar uma linha de cache diretamente da memória introduz latências e, portanto, diminui o desempenho de um programa.

Para o Core i7 (ponte de areia), uma linha de cache contém 64 bytes, portanto, cada acesso à memória recupera 64 bytes. Como o primeiro teste acessa a memória em uma sequência previsível, o processador busca previamente os dados antes de serem realmente consumidos pelo programa. No geral, isso resulta em menos latência nos acessos à memória e, portanto, melhora o desempenho.

Código da amostra:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }
miraculixx
fonte
1

A JVM pode e muitas vezes interfere, e o compilador JIT pode mudar significativamente entre as versões. Algumas micro otimizações são impossíveis em Java devido a limitações de idioma, como ser amigável ao hyperthreading ou a coleção SIMD dos processadores Intel mais recentes.

Recomenda-se a leitura de um blog altamente informativo sobre o assunto de um dos autores do Disruptor :

Sempre se deve perguntar por que se preocupar em usar Java se você deseja micro-otimizações, existem muitos métodos alternativos para acelerar uma função, como usar JNA ou JNI para passar para uma biblioteca nativa.

Steve-o
fonte