Comparação (não totalmente justa) entre Java e C

Eis um dos motivos pelo qual detesto Java… Em C eu posso concatenar duas constantes definidas como símbolos do pré-compilador que o compilador as armazenará como uma string única e, simplesmente, passará o ponteiro para uma função puts() para imprimí-la… Em Java não há jeito de definir símbolos que só serão conhecidos pelo analizador léxico da linguagem, você tem que declará-las em classes de armazenamento, de preferẽncia com o modificador “final”.

Eis um exemplo em C:

#include <stdio.h>

#define STR1 "a"
#define STR2 "b"

void main(int argc, char *argv[])
{
  puts(STR1 STR2);
}

Note que STR1 e STR2 podem ser concatenadas sem o auxílio de qualquer operador. A string que o compilador verá é “ab”. Observando o código gerado pelo compilador, temos:

.section .rodata
.LC0:
.string "ab"

.section .text
.globl main
.type main,@function
main:
  mov rdi,offset .LC0
  jmp puts

Ou seja, o compilador coloca a constante “ab” no segmento “.rodata”, obtém o endereço do primeiro byte da string e coloca-o em RDI e pula para puts. Nada é mais rápido que isso!

Agora, vejamos o mesmo código em Java:

public class Test {
  // As "constantes" precisam ser definidas como 
  // campos privados e finais! Não há como definir 
  // "símbolos" léxicos!
  private static final String STR1 = "a";
  private static final String STR2 = "b";

  public static void main(String[] args) {
    // É necessário usar a operação de concatenação de strings '+'!
    System.out.println(STR1 + STR2);
  }
}

Compile com:

$ javac Test.java

Para ver o que o javac fez, podemos usar o utilitário javap:

$ javap -l -c -verbose -constants Test.class > Test.javap

Eis o resultado (retirei a maioria das informações para não ficar muito grande):

public class Test
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#21         //  java/lang/Object."":()V
   #2 = Fieldref           #22.#23        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #24            //  "ab"
   #4 = Methodref          #25.#26        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   ...
   #7 = Utf8               STR1
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               ConstantValue
  #10 = String             #29            //  "a"
  #11 = Utf8               STR2
  #12 = String             #30            //  "b"
  ...
{
  ...

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2             // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3             // String "ab"
         5: invokevirtual #4             // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return        
}

A função estática main obtém uma referência (ponteiro) para o membro java.io.PrintStream do objeto java.lang.System.out. e o empilha. Logo depois ele pega a referência ao objeto java.lang.String (que, o compilador foi esperto o suficiente para concatená-los!) e a empilha. Então ele “invoca” a função membro println da referência previamente empilhada…

A comparação pode parecer injusta, afinal, o código em C usou a string como um valor literal passado para puts(). Faça o mesmo, em Java, e você obterá o mesmíssimo código!!!

Um detalhe importante: A “máquina virtual” do Java (JVM) não possui o conceito de “registradores”. Tudo tem que ser empilhado e desempilhado. A mesma coisa acontece na instrução invokevirtual, que empilha a referência à função membro e depois faz o que tem que fazer… Fica claro, pra você, que esse treco é, pelo menos, umas 10 vezes mais lento do que o equivalente em C?

Tudo muito bonito e certinho, mas por que diabos as constantes STR1 e STR2 continuam no código final? (Note as referências #10 e #12). A explicação é a seguinte: Java não tem como saber para que diabos essa classe vai ser usada… Mesmo que você defina uma função main() estática, em conformidade com a especificação de applets executáveis, a classe pode ser usada para outro fim!! Então, Java mantém tudo quanto é lixo desnecessário dentro do código final.

A única otimização possível (e apenas neste caso!) foi a concatenação prévia de STR1 e STR2. E isso ocorreu somente porque as defini como final, no corpo da classe. Experimente retirar o final e você verá algo assim:

public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup           
         7: invokespecial #4                  // Method java/lang/StringBuilder."":()V
        10: getstatic     #5                  // Field STR1:Ljava/lang/String;
        13: invokevirtual #6                  // Method java/lang/StringBuilder.append:(...);
        16: getstatic     #7                  // Field STR2:Ljava/lang/String;
        19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(...);
        22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:();
        25: invokevirtual #9                  // Method java/io/PrintStream.println:(String;)V
        28: return

Isso ai é equivalente a:

public static void main(String[] args) {
  StringBuilder sb = new StringBuilder;
  sb.append(STR1);
  sb.append(STR2);
  String str = sb.ToString();
  System.out.println(str);
}

Yep! As strings serão concatenadas separadamente e, para isso, um objeto da classe StringBuilder precisa ser criado no heap! Mesmo que os campos continuem como “private” e não há como acessá-los de fora da função main… Note: Ainda temos apenas duas constantes, strings, que só são usadas em main() e o compilador simplesmente ignora esse fato, criando código inchado (bloated!).

E olha que esse é um exemplo bem simples! Imagina só aquelas aplicações web com trocentas mil classes, usando MVC e sabe-lá-o-que-mais?!

Anúncios

Argh! Java?! Que nojo!

Um amigo me passou essa… Um código em C++, usando STL e um código em Java que fazem a mesmíssima coisa: Somar 32768 inteiros aleatórios de colocados num array… Ordenados e não ordenados… Eis o resultado do tempo gasto:

Array ordenado, C++: 2.56 segundos
Array não ordenado, C++: 2.56 segundos

Array ordenado, Java: 4.25 segundos
Array não ordenado, Java: 7.84 segundos

Sério que Java consome duas vezes mais tempo para percorrer um array, de maneira linear, se o CONTEÚDO estiver fora de ordem?! PUTS!

UPDATE:  Um comportamento similar pode ser observado se compilarmos com nível de otimização -O2 ou inferior. E a explicação é simples… A distribuição dos valores menores que 128 nos itens do array é de, mais ou menos, 50%. No caso do array fora de ordem o processador não tem chance de manter o reconhecimento de padrão do branch prediction atualizado dentro do loop de teste. Ele erra tando quanto acerta. Com a opção de compilação -O3 o if dentro do loop sob teste é substituido por uma instrução CMOV, eliminando o if… O pedaço do código dos ifs ficam assim:

; 'if', com -O2                    ; 'if', com -O3

; EBX = sum                        ; EBX = sum
; EAX = arraySize                  ; EAX = arraySize
; RDX = &amp;data[0]               ; RDX = &amp;data[0]
;                                  ;
  movsx rcx,dword [rdx]              mov    ecx,[rdx]
  cmp   rcx,128                      movsx  r8,ecx
  jl    .skip                        add    r8,rbx
  add   ebx,ecx                      cmp    ecx,128
.skip:                               cmovge rbx,r8

Ou seja, eliminando um salto condicional melhoramos a performance consideravelmente!!!

Os códigos:

/* Em C++: SortedArrayTest.cc

   Compilar com:
     g++ -O3 -mtune=native SortedArrayTest.cc \
         -o SortedArrayTest 
*/
#include <algorithm>
#include <ctime>
#include <iostream>

int main(void)
{
  const unsigned arraySize = 32768;
  int data[arraysize];

  for (unsigned c = 0; c < arraySize; c++)
    data[c] = std::rand() % 256;

  /* Retire essa linha para testar com o 
     array desordenado. */
  std::sort(data, data + arraySize);

  clock_t start = clock();
  long sum = 0;

  for (unsigned i = 0; i < 100000; i++)
  {
    for (unsigned c = 0; c < arraySize; c++)
      if (data[c] >= 128)
        sum += data[c];
  }

  double elapsedTime = 
    static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

  std::cout << elapsedTime << '\n';
  std::cout << "sum = " << sum << '\n';
}

E agora o MESMO código, em Java:

// Em Java: SortedArrayTest.java
//
// Compile com:
//   javac SortedArrayTest.java
//
import java.util.Arrays;
import java.util.Random;

public class SortedArrayTest {
  public static void main(String[] args) {
    int arraySize = 32768;
    int data[] = new int[arraySize];

    Random rnd = new Random(0);
    for (int c = 0; c < arraySize; c++)
      data[c] = rnd.nextInt() % 256;

    /* Retire essa linha para testar o 
       array desordenado. */
    Arrays.sort(data);

    long start = System.nanoTime();
    long sum = 0;

    for (int i = 0; i < 100000; i++) {
      for (int c = 0; c < arraySize; c++) {
        if (data[c] >= 128)
          sum += data[c];
      }
    }

    System.out.println(
      (System.nanoTime() - start) / 1000000000.0);
    System.out.println("sum = " + sum);
  }
}

Você consegue perceber o problema aqui?

Eis uma rotina do Projeto VirtualWorlds:

typedef struct vec3_s {
  float x, y, z;
} vec3_t;

void vec3_cross(vec3_t *vout_p, vec3_t *v1_p, vec3_t *v2_p)
{
  vout_p->x = (v1_p->y * v2_p->z) - (v1_p->z * v2_p->y);
  vout_p->y = (v1_p->z * v2_p->x) - (v1_p->x * v2_p->z);
  vout_p->z = (v1_p->x * v2_p->y) - (v1_p->y * v2_p->x);
}

A rotina acima calcula o produto vetorial de dois vetores (v1 e v2) e coloca o vetor perpendicular em vout.

O problema não é o cálculo, é outro! Vou coloar a resposta nos comentários daqui alguns dias…

Proponho um desafio para os “desenvolvedores”

Antes de chegar ao meu questionamento, eis o desafio:

Construa um programa que jogue o “jogo-da-velha” de forma que um humano (você) jogue contra o computador e, dada a sua habilidade de pensar, o resultado sempre seja um empate…

A possibilidade de você ganhar está excluída porque, ao jogar contra outro humano, a probabilidade de vocẽ ou o seu oponente ganharem é muito pequena (senão, acho, há um sério problema de raciocínio de ambas as partes).

A primeira regra para o desafio é que você não deve consultar estratégias usadas por outros desenvolvedores (ou matemáticos). Ou seja, usar o Google não vale! A solução tem que sair da sua cabeça… A outra regra, óbvia, a medida que você for estudando o problema, é que não vale criar um programa com um caminhão de ifs para testar todas as possibilidades (são 255168 jogadas possíveis!).

A terceira regra é a seguinte: Seu algorítimo tem que valer tanto para o humano começando o jogo quanto para o computador começando…

Proponho esse desafio por dois motivo: Se você encontrar grande dificuldade em concluir a tarefa, como pode esperar desenvolver qualquer sistema complexo?

O segundo motivo: Para desenvolver jogos é interessante que você inicie com os mais simples primeiro. Nada aparentemente mais fácil do que o bom e velho jogo-da-velha, não é? Para aqueles que não estão interessados em desenvolvimento de jogos, este é um bom exercício também…

Ainda, quero demonstrar que os problemas aparentemente simples têm soluções complexas. Como diria H.L. Mencken:

Para todo problema difícil há uma solução simples, fácil, óbvia… e errada!

A sorte está lançada…

Update: As aspas no título do post não é sarcasmo… trata-se apenas da ênfase para qual grupo de pessoas esse desafio é direcionado!