Estou executando o Windows 8.1 x64 com Java 7 atualização 45 x64 (sem Java de 32 bits instalado) em um tablet Surface Pro 2.
O código a seguir leva 1688ms quando o tipo de i é longo e 109ms quando i é um int. Por que long (um tipo de 64 bits) é uma ordem de magnitude mais lento do que int em uma plataforma de 64 bits com uma JVM de 64 bits?
Minha única especulação é que a CPU demora mais para adicionar um inteiro de 64 bits do que um de 32 bits, mas isso parece improvável. Suspeito que Haswell não usa somadores de propagação de ondulação.
Estou executando isso no Eclipse Kepler SR1, btw.
public class Main {
private static long i = Integer.MAX_VALUE;
public static void main(String[] args) {
System.out.println("Starting the loop");
long startTime = System.currentTimeMillis();
while(!decrementAndCheck()){
}
long endTime = System.currentTimeMillis();
System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
}
private static boolean decrementAndCheck() {
return --i < 0;
}
}
Editar: Aqui estão os resultados do código C ++ equivalente compilado pelo VS 2013 (abaixo), mesmo sistema. long: 72265ms int: 74656ms Esses resultados estavam no modo de depuração de 32 bits.
No modo de liberação de 64 bits: long: 875ms long long: 906ms int: 1047ms
Isso sugere que o resultado que observei é a estranheza da otimização da JVM, e não as limitações da CPU.
#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"
long long i = INT_MAX;
using namespace std;
boolean decrementAndCheck() {
return --i < 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout << "Starting the loop" << endl;
unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();
cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;
}
Edit: Apenas tentei isso novamente em Java 8 RTM, nenhuma mudança significativa.
fonte
currentTimeMillis()
, executar código que pode ser totalmente otimizado de maneira trivial, etc., cheira a resultados não confiáveis.long
como contador de loop, porque o compilador JIT otimizou a saída do loop, quando usei umint
. Seria necessário examinar a desmontagem do código de máquina gerado.Respostas:
Minha JVM faz uma coisa bem direta com o loop interno quando você usa
long
s:É cheats, difícil, quando você usa
int
s; primeiro, há algumas coisas complicadas que eu não pretendo entender, mas parece uma configuração para um loop desenrolado:então o próprio loop desenrolado:
em seguida, o código de desmontagem para o loop desenrolado, ele próprio um teste e um loop direto:
Portanto, ele é 16 vezes mais rápido para o ints porque o JIT desenrolou o
int
loop 16 vezes, mas não o desenrolou de formalong
alguma.Para completar, aqui está o código que realmente tentei:
Os dumps de montagem foram gerados usando as opções
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
. Observe que você precisa mexer na instalação da JVM para que isso funcione para você também; você precisa colocar alguma biblioteca compartilhada aleatória exatamente no lugar certo ou ela irá falhar.fonte
long
versão seja mais lenta, mas sim que aint
versão seja mais rápida. Isso faz sentido. Provavelmente, não foi investido tanto esforço para fazer o JIT otimizar aslong
expressões.gcc
usa-f
como opção de linha de comando para "flag", e aunroll-loops
otimização é ativada dizendo-funroll-loops
. Eu apenas uso "unroll" para descrever a otimização.i-=16
, que é 16x mais rápido.A pilha JVM é definida em termos de palavras , cujo tamanho é um detalhe de implementação, mas deve ter pelo menos 32 bits de largura. O implementador de JVM pode usar palavras de 64 bits, mas o bytecode não pode contar com isso e, portanto, as operações com valores
long
oudouble
devem ser tratadas com cuidado extra. Em particular, as instruções de ramificação de inteiro da JVM são definidas exatamente no tipoint
.No caso do seu código, a desmontagem é instrutiva. Este é o bytecode para a
int
versão compilada pelo Oracle JDK 7:Observe que a JVM carregará o valor do seu estático
i
(0), subtrairá um (3-4), duplicará o valor na pilha (5) e o empurrará de volta para a variável (6). Em seguida, ele faz uma comparação com zero e retorna.A versão com
long
é um pouco mais complicada:Primeiro, quando a JVM duplica o novo valor na pilha (5), ela precisa duplicar duas palavras da pilha. No seu caso, é bem possível que isso não seja mais caro do que duplicar um, já que o JVM é livre para usar uma palavra de 64 bits se for conveniente. No entanto, você notará que a lógica do branch é mais longa aqui. A JVM não tem uma instrução para comparar a
long
com zero, então ela precisa colocar uma constante0L
na pilha (9), fazer umalong
comparação geral (10) e então desviar para o valor desse cálculo.Aqui estão dois cenários plausíveis:
long
versão, empurrando e removendo vários valores extras, e estes estão na pilha gerenciada virtual , não na pilha real de CPU assistida por hardware. Se for esse o caso, você ainda verá uma diferença significativa de desempenho após o aquecimento.Eu recomendo que você escreva um microbenchmark correto para eliminar o efeito de ter o JIT ativado, e também tentar isso com uma condição final que não seja zero, para forçar o JVM a fazer a mesma comparação no
int
que faz com olong
.fonte
== 0
, o que parece ser uma parte desproporcionalmente grande dos resultados do benchmark. Parece-me mais provável que o OP esteja tentando medir uma gama mais geral de operações, e essa resposta aponta que o benchmark é altamente inclinado para apenas uma dessas operações.A unidade básica de dados em uma Java Virtual Machine é a palavra. A escolha do tamanho correto da palavra é deixada após a implementação da JVM. Uma implementação JVM deve escolher um tamanho mínimo de palavra de 32 bits. Ele pode escolher um tamanho de palavra maior para ganhar eficiência. Também não há nenhuma restrição de que uma JVM de 64 bits deve escolher apenas palavras de 64 bits.
A arquitetura subjacente não determina que o tamanho da palavra também seja o mesmo. JVM lê / grava dados palavra por palavra. Esta é a razão por que ele pode ser levando mais tempo para uma longa do que um int .
Aqui você pode encontrar mais informações sobre o mesmo assunto.
fonte
Acabei de escrever um benchmark usando compasso de calibre .
Os resultados são bastante consistentes com o código original: uma aceleração de ~ 12x para usar
int
overlong
. Certamente parece que o desenrolamento de loop relatado por tmyklebu ou algo muito semelhante está acontecendo.Este é o meu código; observe que ele usa um instantâneo recém-criado de
caliper
, já que não consegui descobrir como codificar em relação à versão beta existente.fonte
Para que conste, esta versão faz um "aquecimento" bruto:
Os tempos gerais melhoram cerca de 30%, mas a proporção entre os dois permanece praticamente a mesma.
fonte
int
é 20 vezes mais rápido) com este código.Para os registros:
se eu usar
(alterado "l--" para "l = l - 1l") o desempenho longo melhora em ~ 50%
fonte
Não tenho uma máquina de 64 bits para testar, mas a diferença bastante grande sugere que há mais do que o bytecode um pouco mais longo em ação.
Vejo tempos muito próximos para long / int (4400 vs 4800ms) no meu 1.7.0_45 de 32 bits.
Isso é apenas uma suposição , mas suspeito fortemente que seja o efeito de uma penalidade de desalinhamento de memória. Para confirmar / negar a suspeita, tente adicionar um public static int dummy = 0; antes da declaração de i. Isso empurra i para baixo em 4 bytes no layout de memória e pode torná-lo devidamente alinhado para melhor desempenho.Confirmado que não está causando o problema.EDITAR:
O raciocínio por trás disso é que o VM não pode reordenar os campos em seu lazer adicionando preenchimento para alinhamento ideal, uma vez que isso pode interferir com JNI(Não é o caso).fonte