Archive for the ‘Comentários’ Category.

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

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

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 (…)”

Primeiro post

Dominós

“See you in another life, Brother!”

O primeiro post a gente nunca esquece…

Na verdade nesse post ainda não começo a falar de C++ propriamente dito, mas de como os posts serão organizados nas categorias.

As categorias não são mutuamente exclusivas. E nem definitivas, novas categorias surgirão com o tempo.

No momento, pretendo organizar os posts nas seguintes categorias:

  1. Técnicas de programação
  2. Estilos de programação
  3. Bibliotecas
  4. Extensões funcionais
  5. Templates
  6. Metaprogramação
  7. EDSL (Embedded Domain Specific Language)
  8. Tips&Tricks
  9. Comentários
  10. C++ básico
  11. Devaneios…   :)

A categoria Devaneios é onde colocarei aquelas idéias onde a primeira pergunta que me vem à cabeça é: será que isso vai compilar?

Fui!