Um caso para manter primitivas em Java

os primitivos fazem parte da linguagem de programação Java desde seu lançamento inicial em 1996 e, no entanto, continuam sendo um dos recursos de linguagem mais controversos. John Moore faz um forte argumento para manter primitivos na linguagem Java, comparando benchmarks Java simples, com e sem primitivos. Ele então compara o desempenho do Java com o de Scala, C++ e JavaScript em um tipo específico de aplicativo, onde os primitivos fazem uma diferença notável.

pergunta: Quais são os três fatores mais importantes na compra de imóveis?
resposta: localização, Localização, Localização.

este ditado antigo e frequentemente usado destina-se a implicar que a localização domina completamente todos os outros fatores quando se trata de imóveis. Em um argumento semelhante, os três fatores mais importantes a serem considerados para usar tipos primitivos em Java são Desempenho, Desempenho, Desempenho. Existem duas diferenças entre o argumento para imóveis e o argumento para primitivos. Primeiro, com imóveis, a localização Domina em quase todas as situações, mas os ganhos de desempenho do uso de tipos primitivos podem variar muito de um tipo de aplicação para outro. Em segundo lugar, com imóveis, existem outros fatores a serem considerados, embora geralmente sejam menores em comparação com a localização. Com tipos primitivos, há apenas um motivo para usá — los-desempenho; e somente se o aplicativo for do tipo que pode se beneficiar de seu uso.

as primitivas oferecem pouco valor para a maioria dos aplicativos relacionados a negócios e da Internet que usam um modelo de programação cliente-servidor com um banco de dados no back-end. Mas o desempenho de aplicativos dominados por cálculos numéricos pode se beneficiar muito do uso de primitivos.

a inclusão de primitivos em Java tem sido uma das decisões de design de linguagem mais controversas, como evidenciado pelo número de artigos e postagens de Fórum relacionadas a esta decisão. Simon Ritter observou em sua palestra no Jax London em novembro de 2011 que uma séria consideração estava sendo dada à remoção de primitivos em uma versão futura do Java (ver slide 41). Neste artigo, apresentarei brevemente primitivos e o sistema de tipo duplo do Java. Usando amostras de código e benchmarks simples, vou defender por que as primitivas Java são necessárias para certos tipos de aplicativos. Também compararei o desempenho do Java com o de Scala, C++ e JavaScript.

primitivos versus objetos

como você provavelmente já sabe se está lendo este Artigo, Java tem um sistema de tipo duplo, geralmente referido como tipos primitivos e tipos de objetos, muitas vezes abreviado simplesmente como primitivos e objetos. Existem oito tipos primitivos predefinidos em Java e seus nomes são palavras-chave reservadas. Exemplos comumente usados incluem int, double e boolean. Essencialmente, todos os outros tipos em Java, incluindo todos os tipos definidos pelo usuário, são tipos de objetos. (Eu digo “essencialmente” porque os tipos de array são um pouco híbridos, mas são muito mais parecidos com tipos de objetos do que com tipos primitivos.) Para cada tipo primitivo, há uma classe wrapper correspondente que é um tipo de objeto; os exemplos incluem Integer para int, Double para double e Boolean para boolean.

os tipos primitivos são baseados em valor, mas os tipos de objetos são baseados em referência, e aí reside tanto o poder quanto a fonte de controvérsia dos tipos primitivos. Para ilustrar a diferença, considere as duas declarações abaixo. A primeira declaração usa um tipo primitivo e a segunda usa uma classe wrapper.

int n1 = 100;Integer n2 = new Integer(100);

Usando o autoboxing, um recurso adicionado ao JDK 5, eu poderia encurtar a segunda declaração simplesmente

Integer n2 = 100;

mas a semântica subjacente não mudam. Autoboxing simplifica o uso de classes wrapper e reduz a quantidade de código que um programador tem que escrever, mas não muda nada em tempo de execução.

A diferença entre o primitivo n1 e o objeto wrapper n2 é ilustrada pelo diagrama na Figura 1.

John I. Moore, Jr.

Figura 1. Layout de memória primitivos versus objetos

A variável n1 contém um valor inteiro, mas a variável n2 contém uma referência para um objeto, e ele é o objeto que contém o valor inteiro. Além disso, o objeto referenciado por n2 também contém uma referência ao objeto de classe Double.

O problema com os primitivos

Antes de eu tentar convencê-lo da necessidade de tipos primitivos, devo reconhecer que muitas pessoas não concordam comigo. Sherman Alpert em” tipos primitivos considerados prejudiciais “argumenta que os primitivos são prejudiciais porque misturam” semântica processual em um modelo orientado a objetos uniforme. Primitivos não são objetos de primeira classe, mas existem em uma linguagem que envolve, principalmente, objetos de primeira classe. Primitivos e objetos (na forma de classes wrapper) fornecem duas maneiras de lidar com tipos logicamente semelhantes, mas eles têm semântica subjacente muito diferente. Por exemplo, como duas instâncias devem ser comparadas para igualdade? Para tipos primitivos, usa-se o operador ==, mas para objetos a escolha preferida é chamar o método equals(), que não é uma opção para primitivos. Da mesma forma, existem diferentes semânticas ao atribuir valores ou passar parâmetros. Mesmo os valores padrão são diferentes; por exemplo, 0 para int versus null para Integer.

para mais informações sobre esta questão, veja o post do blog de Eric Bruno, “A modern primitive discussion”, que resume alguns dos prós e contras dos primitivos. Uma série de discussões sobre Stack Overflow também se concentram em primitivas, incluindo “Por que as pessoas ainda usam tipos primitivos em Java?”e” há uma razão para sempre usar objetos em vez de primitivos?.”Programadores Stack Exchange hospeda uma discussão semelhante intitulada” quando usar a classe vs primitiva em Java?”.

Utilização da memória

A double em Java sempre ocupa 64 bits na memória, mas o tamanho de uma referência depende da máquina virtual Java (JVM). Meu computador executa a versão de 64 bits do Windows 7 e uma JVM de 64 bits e, portanto, uma referência no meu computador ocupa 64 bits. Com base no diagrama na Figura 1 que eu esperaria de um único double como n1 para ocupar 8 bytes (64 bits), e eu esperaria um único Double como n2 para ocupar 24 bytes — 8 para a referência para o objeto, de 8 a double valor armazenado no objeto, e 8 para a referência para o objeto da classe para Double. Além disso, o Java usa memória extra para suportar a coleta de lixo para tipos de objetos, mas não para tipos primitivos. Vamos ver.

usando uma abordagem semelhante à De Glen McCluskey em ” tipos primitivos Java vs. wrappers, ” o método mostrado na Listagem 1 mede o número de bytes ocupados por uma matriz n Por n (matriz bidimensional) de double.

Listagem 1. Calculando a utilização da memória do tipo duplo

modificando o código na Listagem 1 com as mudanças de tipo óbvias (não mostradas), também podemos medir o número de bytes ocupados por uma matriz n-Por-n de Double. Quando testo esses dois métodos no meu computador usando matrizes 1000 por 1000, obtenho os resultados mostrados na Tabela 1 abaixo. Como ilustrado, a versão para o tipo primitivo double equivale a um pouco mais de 8 bytes por entrada na matriz, aproximadamente o que eu esperava. No entanto, a versão para o tipo de objeto Double exigia um pouco mais de 28 bytes por entrada na matriz. Assim, neste caso, a utilização da memória de Double é mais de três vezes a utilização da memória de double, o que não deve ser uma surpresa para quem entende o layout da memória ilustrado na Figura 1 acima.

desempenho de tempo de execução

para comparar os desempenhos de tempo de execução para primitivos e objetos, precisamos de um algoritmo dominado por cálculos numéricos. Para este artigo, escolhi a multiplicação de matrizes e computo o tempo necessário para multiplicar duas matrizes 1000 por 1000. Eu codifiquei a multiplicação da matriz para double de maneira direta, conforme mostrado na Listagem 2 abaixo. Embora possa haver maneiras mais rápidas de implementar a multiplicação de matrizes (talvez usando simultaneidade), esse ponto não é realmente relevante para este artigo. Tudo o que preciso é de código comum em dois métodos semelhantes, um usando o primitivo double e outro usando a classe wrapper Double. O código para multiplicar duas matrizes do tipo Double é exatamente assim na Listagem 2 com as mudanças de tipo óbvias.

Listagem 2. Multiplicando duas matrizes do tipo double

executei os dois métodos para multiplicar duas matrizes 1000 por 1000 no meu computador várias vezes e medi os resultados. Os tempos médios são mostrados na Tabela 2. Assim, neste caso, o desempenho de tempo de execução de double é mais de quatro vezes mais rápido que o de Double. Isso é simplesmente muita diferença para ignorar.

o benchmark SciMark 2.0

até agora, usei o benchmark simples e simples de multiplicação de matrizes para demonstrar que as primitivas podem produzir um desempenho de computação significativamente maior do que os objetos. Para reforçar minhas afirmações, usarei uma referência mais científica. SciMark 2.0 é um benchmark Java para computação científica e numérica disponível no Instituto Nacional de padrões e Tecnologia (NIST). Eu baixei o código-fonte para este benchmark e criei duas versões, a versão original usando primitivos e uma segunda versão usando classes wrapper. Para a segunda versão, substituí int por Integer e double por Double para obter o efeito total do uso de classes wrapper. Ambas as versões estão disponíveis no código-fonte deste artigo.

download

John I. Moore, Jr.

o benchmark SciMark mede o desempenho de várias rotinas computacionais e relata uma pontuação composta em Mflops aproximados (milhões de operações de ponto flutuante por segundo). Assim, números maiores são melhores para este benchmark. A tabela 3 fornece as pontuações compostas médias de várias execuções de cada versão deste benchmark no meu computador. Como mostrado, os desempenhos de tempo de execução das duas versões do benchmark SciMark 2.0 foram consistentes com os resultados de multiplicação de matrizes acima, pois a versão com primitivos era quase cinco vezes mais rápida do que a versão usando classes wrapper.

você viu algumas variações de programas Java fazendo cálculos numéricos, usando um benchmark caseiro e mais científico. Mas como o Java se compara a outras linguagens? Concluirei com uma rápida olhada em como o desempenho do Java se compara ao de três outras linguagens de programação: Scala, C++ e JavaScript.

Benchmarking Scala

Scala é uma linguagem de programação que roda na JVM e parece estar ganhando popularidade. Scala tem um sistema de tipo unificado, o que significa que não distingue entre primitivos e objetos. De acordo com Erik Osheim na classe de tipo numérico do Scala (Pt. 1), Scala usa tipos primitivos quando possível, mas usará objetos, se necessário. Da mesma forma, a descrição de Martin Odersky das matrizes de Scala diz que”… um array Scala Array é representado como um Java int, um Array é representado como um Java double…”

então, isso significa que o sistema de tipo unificado do Scala terá desempenho de tempo de execução comparável aos tipos primitivos do Java? Vamos ver.

não sou tão proficiente com Scala quanto com Java, mas tentei converter o código para o benchmark de multiplicação de matrizes diretamente de Java para Scala. O resultado é mostrado na Listagem 3 abaixo. Quando executei a versão Scala do benchmark no meu computador, ela teve uma média de 12,30 segundos, o que coloca o desempenho do Scala muito próximo do de Java com primitivos. Esse resultado é muito melhor do que eu esperava e suporta as alegações sobre como o Scala lida com tipos numéricos.Como baixar e instalar Minecraft 1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1. Multiplicando duas matrizes em Scala

Benchmarking C++

como o C++ é executado diretamente em “bare metal” em vez de em uma máquina virtual, seria de esperar naturalmente que o c++ fosse executado mais rápido que o Java. Além disso, o desempenho do Java é reduzido ligeiramente pelo fato de o Java verificar acessos a matrizes para garantir que cada índice esteja dentro dos limites declarados para a matriz, enquanto o C++ não (um recurso C++ que pode levar a estouros de buffer, que pode ser explorado por hackers). Achei o c++ um pouco mais estranho do que o Java ao lidar com matrizes bidimensionais básicas, mas felizmente muito desse constrangimento pode ser escondido dentro das partes privadas de uma classe. Para C++, criei uma versão simples de uma classe Matrix e sobrecarregei o operador * para multiplicar duas matrizes, mas o algoritmo básico de multiplicação de matrizes foi convertido diretamente da versão Java. O código-fonte C++ é mostrado na Listagem 4.Como baixar e instalar Minecraft 1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1. Multiplicando duas matrizes em C++

usando Eclipse CDT (Eclipse para desenvolvedores C++) com o compilador MinGW C++, é possível criar versões de depuração e lançamento de um aplicativo. Para testar C++, executei a versão de lançamento várias vezes e calculei a média dos resultados. Como esperado, C++ correu visivelmente mais rápido neste benchmark simples, com média de 7,58 segundos no meu computador. Se o desempenho bruto é o principal fator para selecionar uma linguagem de programação, então C++ é a linguagem de escolha para aplicativos numericamente intensivos.

Benchmarking JavaScript

Ok, este me surpreendeu. Dado que JavaScript é uma linguagem muito dinâmica, esperava que seu desempenho fosse o pior de todos, ainda pior do que Java com classes wrapper. Mas, na verdade, o desempenho do JavaScript estava muito mais próximo do de Java com primitivos. Para testar o JavaScript, instalei o Node.js, um mecanismo JavaScript com a reputação de ser muito eficiente. Os resultados tiveram uma média de 15,91 segundos. A listagem 5 mostra a versão JavaScript do benchmark de multiplicação de matrizes que executei no Node.como baixar e instalar Minecraft 1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1. Multiplicando duas matrizes em JavaScript

em conclusão

quando o Java chegou à cena há cerca de 18 anos, não era a melhor linguagem de uma perspectiva de desempenho para aplicativos dominados por cálculos numéricos. Mas com o tempo, com avanços tecnológicos em áreas como compilação just-in-time (JIT) (também conhecida como compilação adaptativa ou dinâmica), o desempenho do Java para esses tipos de aplicativos agora é comparável ao de linguagens que são compiladas em código nativo quando tipos primitivos são usados.Além disso, as primitivas eliminam a necessidade de coleta de lixo, proporcionando assim outra vantagem de desempenho das primitivas sobre os tipos de objetos. A tabela 4 resume o desempenho de tempo de execução do benchmark de multiplicação de matrizes no meu computador. Outros fatores, como manutenção, portabilidade e experiência do desenvolvedor, tornam o Java uma escolha melhor para muitos desses aplicativos.

conforme discutido anteriormente, a Oracle parece estar dando séria consideração à remoção de primitivos em uma versão futura do Java. A menos que o compilador Java possa gerar código com desempenho comparável ao dos primitivos, acho que sua remoção do Java impediria o uso do Java para certas classes de aplicativos; ou seja, aqueles aplicativos dominados por cálculos numéricos. Neste artigo, usei um benchmark simples baseado na multiplicação de matrizes e um benchmark mais científico, SciMark 2.0, para argumentar esse ponto.

sobre o autor

John I. Moore, Jr., Professor de Matemática e Ciência da Computação na Citadel, tem uma ampla gama de experiência na indústria e na academia, com experiência específica nas áreas de tecnologia orientada a objetos, engenharia de software e Matemática Aplicada. Por mais de três décadas, ele projetou e desenvolveu software usando bancos de dados relacionais e várias linguagens de alta ordem, e ele trabalhou extensivamente em Java desde a versão 1.1. Além disso, ele desenvolveu e ministrou vários cursos acadêmicos e seminários industriais sobre tópicos avançados em Ciência da computação.

Leitura adicional

  1. Paul Krill escreveu sobre os planos de Longo Alcance da Oracle para Java em “Oracle estabelece intenções Java de longo alcance” (JavaWorld, Março de 2012). Este artigo, junto com o tópico de comentários associados, me motivou a escrever essa defesa de primitivos.
  2. Szymon Guz escreve sobre seus resultados em benchmarking de tipos primitivos e classes de wrapper em” primitivos e benchmark de objetos em Java ” (SimonOnSoftware, janeiro de 2011).
  3. no site de suporte para programação — princípios e práticas usando C++ (Addison-Wesley, 2009), o criador do C++ Bjarne Stroustrup fornece uma implementação para uma classe matrix que é muito mais completa do que a que acompanha este artigo.John Rose, Brian Goetz e Guy Steele discutem um conceito chamado tipos de valor em “estado dos valores” (OpenJDK.net, abril de 2014). Os tipos de valor podem ser considerados como tipos de agregados definidos pelo Usuário imutáveis sem identidade, combinando propriedades de objetos e primitivos. O mantra para tipos de valor é ” Códigos como uma classe, funciona como um int.”

Deixe uma resposta

O seu endereço de email não será publicado.