Java mais rápido que C++?
Deixemos as “religiões” de lado…
C++ é mais rápido que Java? É uma briga de foice… quase uma briga de torcida organizada! O defensores de ambas as linguagens estão armados até os dentes com os mais variados argumentos. O assunto suscita discussões apaixonadas e intermináveis, com acusações de ambos os lados de imparcialidade nos testes. É possível encontrar diversos benchmarks que fazem a vantagem pender para uma ou outra linguagem. Nesse aqui, por exemplo, o vencedor é o Java. Nesse outro, usando os mesmos testes os resultados são francamente favoráveis ao C++.
O que quero passar aqui é que essa pergunta praticamente não procede. Já vai longe o tempo em que Java era somente interpretado. Hoje existe o JIT, o que torna Java uma linguagem compilada. E comparar duas linguagens compiláveis significa comparar compiladores, mais especificamente, otimizadores de código. São poucas as construções que podem interferir no desempenho e que não podem ser otimizadas. A verdadeira discussão é sobre qual é o melhor otimizador.
E aí as notícias não são nada boas para os defensores do desempenho superior do C++. SUN e IBM derramaram milhões de dólares em suas máquinas virtuais, cuja parte mais importante é justamente o otimizador. O resultado foi surpreendente. As últimas JVMs da IBM e da SUN conseguem executar algunas exemplos de programas Java com desempenho superior aos compiladores C++ disponíveis, em particular o tão popular e querido g++. Em algumas situações específicas conseguem até superar o compilador Intel, reconhecidamente um dos compiladores mais otimizados do mercado.
Mas se agora a discussão é somente sobre qual é o melhor otimizador, faz sentido falar em C++ mais rápido que Java? A verdade é que sim, faz sentido. Java possui algumas características que tornam determinadas construções mais lentas do que as equivalentes em C++. Por outro lado, C++ não possui nenhuma característica que o torne obrigatoriamente mais lento que nenhuma linguagem – pelo contrário, inlines, templates e functores acabam tornando o código mais fácil de ser otimizado. Por outro lado, o abuso de ponteiros e aliases torna o trabalho do otimizador bem mais difícil – problema que o Java não possui.
Mas que características são essas que tornam o Java mais lento? Não são muitas. Mas a verificação de limites de índices de array é uma que realmente faz diferença. O Java, por definição da linguagem, não deve permitir acesso fora dos limites de um array. C++, ao contrário, estabelece claramente que nenhuma verificação é feita neste caso, ficando a cargo do programador garantir que nenhum acesso ilegal será feito. Essa é uma situação onde não há nenhum otimizador que dê jeito, pois os testes para verificar se os índices estão dentro dos limites do array têm de ser feitos em tempo de execução e consomem ciclos de CPU. O Java somente pode dispensar os testes de limites de array se o compilador puder deduzir que não haverá acesso fora dos limites.
Antes de sair por aí gritando “urra!” e “ahá! eu não disse?”, vamos analisar alguns pontos. Alguém pode argumentar que esses testes são necessários. De fato, muitas vezes o são, e o C++ possui tipos abstratos de dados que substituem arrays e fazem esse tipo de verificação em tempo de execução. Mas a questão não é essa. É que em C++ é possível usar ou não esse tipo de verificação, ao passo que em Java não se tem essa opção. Em C++ o programador pode decidir se quer ou não realizar testes de limites de array.
Me lembro da época em que programava em Pascal, onde enquanto se estava depurando o programa compilávamos com a opção “range check error” ligada, mas quando os testes indicavam que o programa estava ok, nós a desligávamos. Nesse ponto o Java trata todo programa como se estivesse em fase de testes – ou todo programador como irresponsável. Diferentemente do C++ onde podemos adotar a mesma estratégia que adotávamos em Pascal.
O Java precisa desse teste por uma questão de princípios: um dos objetivos iniciais da linguagem era a criação de applets, pequenos trechos de código que executam no browser. É necessário limitar a possibilidade dos applets causarem danos, de modo que podemos dizer que a JVM não confia no código que lhe é passado.
O preço que se paga por essa decisão podemos avaliar no exemplo abaixo, onde é mostrada uma multiplicação de matrizes 1000×1000 em Java. O código foi feito da maneira mais direta possível, na verdade fora a parte da inicialização os comandos são os mesmos em Java e em C++, tirados dos posts anteriores sobre otimização. Acho que ninguém pode questionar esse código, dizendo que ele favorece uma ou outra linguagem.
Código original sem nenhuma otimização:
public class Teste {
static final int N = 1000;
public static void main(String[] args) {
double a[][] = new double[N][N],
b[][] = new double[N][N],
c[][] = new double[N][N];
java.util.Random rn = new java.util.Random();
for( int i = 0; i < N; i++ )
for( int j = 0; j < N; j++ ) {
a[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
b[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
c[i][j] = 0;
}
for( int i = 0; i < N; i++ )
for( int j = 0; j < N; j++ )
for( int k = 0; k < N; k++ )
c[i][j] += a[i][k] * b[k][j];
}
}
|
Detalhes:
Esse código foi executado com duas variações de linha de comando:
java Teste java -server -Xbatch Teste
A primeira roda no modo client, que não realiza muitas das otimizações do JIT e por isso é um pouco mais lenta. Já a segunda usa o modo server, o que se traduz em um maior consumo de memória e de tempo para inicialização, porém executa mais rápido (algumas vezes bem mais rápido!). No nosso teste os resultados estão no gráfico a seguir, mostrando também os resultados obtidos para o C++ nos posts anteriores.

Multiplicação de Matrizes
De cara vemos que o compilador Intel C++ é realmente insuperável… 44.48 x mais rápido que o código Java client. Mas mesmo comparando com o g++, vemos que o código C++ é 1.73x mais rápido que o Java client e 1,64x mais rápido que o Java server. Essa diferença pode ser creditada ao inúmeros testes de limites de array que o Java faz e que o C++ não: a cada acesso a um elemento de um array o Java irá verificar se o(s) índice(s) estão dentro dos limites. Embora que, analisando o programa fonte, ambas as linguagens fazem parte do teste quando comparam “i < N”, “j < N” e “k < N” em cada iteração dos comandos for, pois N é justamente o limite superior do array. Só o teste se i, j e k são maiores que zero (o limite inferior de cada array) é que o C++ não faz – mas esse teste é dispensável se a variável de índice for unsigned int. Ou seja, ainda há espaço para o otimizador do Java recuperar parte do tempo perdido.
Mas que tal se realizarmos as mesmas otimizações que fizemos nos posts anteriores? A primeira delas é inverter a ordem dos loops para tirar proveito da arquitetura do cache da CPU.
Código invertendo a ordem dos loops:
public class Teste {
static final int N = 1000;
public static void main(String[] args) {
double a[][] = new double[N][N],
b[][] = new double[N][N],
c[][] = new double[N][N];
java.util.Random rn = new java.util.Random();
for( int i = 0; i < N; i++ )
for( int j = 0; j < N; j++ ) {
a[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
b[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
c[i][j] = 0;
}
for( int i = 0; i < N; i++ )
for( int k = 0; k < N; k++ )
for( int j = 0; j < N; j++ )
c[i][j] += a[i][k] * b[k][j];
}
}
|
Ótimas notícias!!! Parece que agora o otimizador Java compreendeu que não precisa testar os limites do array. Usando o Java server o tempo praticamente se iguala ao g++: 2.77 segundos. Na verdade, 0.02 segundos mais rápido que o g++ com opção “-march=pentium4″. Quando compilamos com o g++ usando a opção “-march=k8″, o tempo do g++ cai para 2.30 segundos (o tempo para esta versão do programa com a opção “-march=k8″ não apareceu em nenhum post anterior). Por último, a opção Java client é desastrosa: 11.37 segundos…
Mas vamos terminar o serviço. Nossa última otimização do código C++ foi feita nesse post, e consiste em criar uma função para executar o loop mais interno. A vantagem dessa função é que ela fixa duas linhas e um elemento das matrizes, facilitando o trabalho do otimizador.
Código com função auxiliar:
public class Teste {
static final int N = 1000;
static void multiplicaPorLinha( double[] ci, double aik, double[] bk ) {
for( int j = 0; j < N; j++ )
ci[j] += aik * bk[j];
}
public static void main(String[] args) {
double a[][] = new double[N][N],
b[][] = new double[N][N],
c[][] = new double[N][N];
java.util.Random rn = new java.util.Random();
for( int i = 0; i < N; i++ )
for( int j = 0; j < N; j++ ) {
a[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
b[i][j] = rn.nextInt()/(rn.nextInt()+0.1);
c[i][j] = 0;
}
for( int i = 0; i < N; i++ )
for( int k = 0; k < N; k++ )
multiplicaPorLinha( c[i], a[i][k], b[k] );
}
}
|
Voilà!! Agora o Java ficou realmente competitivo: 2.00 segundos. Essa versão compilada com o g++ apresenta tempos de 1.80 e 1.62 segundos para as opções “-march=k8″ e “-march=core2″, respectivamente (essa última disponível somente a partir de versões mais recentes do g++). Cabe ainda dizer que, se incluirmos manualmente no código do C++ o teste de limite de array o tempo não se altera! O otimizador do g++ saca que o teste é desnecessário…
Nesse quesito específico o otimizador do C++ consegue superar bastante o otimizador do Java, mesmo que seja incluído manualmente o teste de limites de array no código. Não tem muito jeito, é uma restrição da linguagem… embora mesmo assim o impacto tenha sido pequeno após as otimizações (desnecessárias no caso do compilador Intel…). A propósito, essa última otimização surpreendentemente torna o código gerado pelo compilador Intel mais lento.
Conclusão:
Embora o Java tenha algumas restrições na especificação da linguagem que podem prejudicar o desempenho, esse ponto não é tão fraco assim – nada que um pouco de ajuda ao compilador não minimize. No último teste, apenas 14% mais lento que o g++, e no segundo teste se iguala ao g++ opção “-march=pentium4″. Em termos de desempenho, a questão é mesmo uma briga de otimizadores. A linguagem em si não é mais lenta nem mais rápida, apenas o otimizador não está dando conta do recado (ainda). E use sempre que possível a opção “-server” no Java!
Segue um gráfico comparativo dos experimentos:

Multiplicação de Matrizes em C++ e Java - tempo em segundos
Em outro post irei mostrar uma situação onde o Java ganha (e bem!) do C++… pois é, já existe isso!!!
“Fé inabalável só o é a que pode encarar de frente a razão (…)”
Há muito tempo foi feito um port do Quake 2 (cujo código foi aberto) para Java, usando OpenGL. Aqui estão as informações e o jogo para ser baixado (o link leva direto ao benchmark, que mostra um caso onde Java ficou mais rápido): http://bytonic.de/html/benchmarks.html
Seria interessante, Zimbrão, fazer benchmarks relacionando o consumo de memória, comparado ao ganho de tempo, assim como a diferença de tempo de start up.
Não discuto (e uso!) a performance de Java do lado Servidor, onde a memória normalmente é dedicada a uma aplicação apenas, e que o tempo de start up se perde comparado a um sistema que vá rodar por meses a fio. Mas esses quesitos atrapalham um bocado a utilização de Java no desenvolvimento de aplicações Desktop.
Seria interessante também ver como o otimizador da JVM se comporta com linguagens cujos compiladores são mais complexos (como Scala).
No geral, um artigo bem interessante! Em especial porque as otimizações usadas aqui podem encontrar lugar em várias aplicações que desenvolvemos no dia a dia.
Obrigado!
Pois é… baixar de 26 segundos para 2 segundos é muito bom né?
Espero só contribuir um pouco para que as pessoas “culpem” os otimizadores e não as linguagens…
Java em geral tem um desempenho melhor que o C++ em chamadas recursivas… vou fazer um post sobre isso.
O caso é que não sou um expert em Java, então me atenho ao básico pois certamente qualquer código que eu fizer será mais bem feito em C++… para fazer o equivalente em Java teria de estudar muito Java!
Imagino que você já tenha visto, mas uma linguagem extremamente interessante (que eu mencionei no meu comment anterior e que roda sobre a JVM) é Scala[0]. Orientação a Objetos, tipagem forte com uso *intensivo* de inferência de tipos e diversas construções funcionais.
Até consegui ver uma implementação de Quick Sort em Scala[1] que não perde muito pouco em legibilidade pra Haskell.
Me impressionou tanto que eu acabei voltando mais a minha atenção para a JVM recentemente.
[0]: http://www.scala-lang.org/
[1]: http://en.literateprograms.org/Quicksort_(Scala) — A segunda versão nesta página.
Hehe,
para um Blog de C++ já estou falando muito de Java… se começar a falar de Scala acho que fugirei do tema principal. Deixo a bola com o Peter, ele tem um Blog sobre Java: http://craftnicely.blogspot.com/
[]s
Opa! Eu vou ter que deixar pra outro. O blog é mais voltado a Engenharia de Software teórica e prática. Técnicas de desenvolvimento, modelagem, arquitetura, metodologias, processos, modelos, normas, etc. De vez em quando aparece código como exemplo e aí eu uso Java, mas o blog não fala de linguagem de programação nenhuma, então falar de Scala acaba fugindo do escopo também.
Abraço
Para fazer um teste realmente válido você deveria ter feito um programa mais simples, sem uso de APIs como java.util.Random. Poderia fazer um cálculo complexo, por exemplo.
Acho que não é esse o problema… a parte do código que usa Random é um loop executado um milhão de vezes, enquanto que a multiplicação de matrizes propriamente dita são 2 bilhões de cálculo de índices de array mais 1 bilhão de multiplicações e 1 bilhão de somas. Essa é a parte que consome tempo.
Se substituirmos essa parte por:
for( int i = 0; i < N; i++ )
for( int j = 0; j < N; j++ ) {
a[i][j] = i/(j+0.1);
b[i][j] = j/(i+0.1);
c[i][j] = 0;
}
O tempo no Java cai para 1.90 segundos (diminuiu apenas 0.10 segundo) e no C++, cai para 1.75 e 1.57, de acordo com a versão e parâmetros do g++ acima, ou seja, caiu 0.05 segundo.
Apenas essa diferença de 0.05 segundo de ganho entre o Java e o C++ quando paramos de usar Random é que pode ser creditada como overhead da API do Java (o quanto a random do Java é mais lenta que a random do C++).
Animal o ICC.
muito mesmo.
No linux, passando as opções de otimização certas, li na documentação do g++ que ele também faz a inversão da ordem do loop. Não testei, e no windows essa funcionalidade ainda não está disponível – pelo menos no MingW que eu tenho aqui.
Mas já já o g++ encosta no Intel. Para outros programas, o Fibonacci por exemplo, o g++ foi mais rápido!