Objetos String da JavaScript e performance
Publicado em: 2012-12-6 — 14.545 visualizacoes
Créditos: Este artigo é um tradução. O artigo original de autoria de Panagiotis Tsalaportas foi publicado em: Performance with JavaScript String Objects no site da Mozilla.
Introdução
Essa matéria se propõe a analisar a performance de engines JavaScript no tratamento de strings definidas como valores primitivos e como objetos. Trata-se de uma análise de desempenho das funcionalidades relacionadas ao que foi exposto no excelente artigo de Kiro Risk: The Wrapper Object. Antes de continuar a leitura eu sugiro ler o artigo de Kiro à título de introdução a essa matéria..
A especificação ECMAScript 5.1 Language Specification (PDF link) no parágrafo 4.3.18 diz o seguinte sobre o objeto String:
O objeto String é membro do tipo Objeto, ou seja, é uma instância padrão do construtor nativo String.
NOTA Um objeto String é criado com uso do construtor String em uma expressão contendo o operador
new
seguida do valor da string como argumento.
O objeto resultante tem uma propriedade interna que retorna o valor da string. Um objeto String pode ser transformado em um valor primitivo de string por coerção com uso do construtor String como uma função (15.5.1).
David Flanagan no seu excelente livro “JavaScript: The Definitive Guide”, na seção 3.6 descreve o objeto provisório (Wrapper Objeto) assim:
Strings não são objetos, então porque elas tem propriedades? Sempre que você se refere a uma propriedade de uma string, JavaScript converte seu valor para um objeto, tal como se tivéssemos chamando new String(s). […] Uma vez que a propriedade tenha sido resolvida, o novo objeto criado é descartado. (Implementações não são obrigadas a criar e descartar esses objetos provisórios, elas devem adotar comportamento segundo seus próprios critérios.)
É importante prestar atenção ao texto anterior em negrito. Basicamente, as diferentes maneiras de como um objeto new String é criado depende da implementação. Assim sendo, um questionamento válido é o seguinte: “uma vez que o valor primitivo de uma string é tornado um objeto String por coerção quando se tenta acessar uma propriedade, por exemplo str.length
, não seria mais conveniente se tivéssemos declarado a variável como um objeto String?”. Dito de outra forma: Não seria melhor declarar a variável como str = new String("hello")
em lugar de declarar como valor primitivo, ou seja var str = "hello"
e assim evitar que a engine JS criasse um objeto new String para acessar suas propriedades?
Aqueles que lidam com a implementação da especificação ECMAScript em engines JS já conhecem a resposta a esse questionamento, contudo vale a pena analisar com mais detalhes o que há por trás da tão comum sugestão: “Não crie números ou strings com uso do operador new
”.
Nossas demonstrações e objetivos
Para nossas demonstrações usaremos os navegadores Firefox e Chrome, mas os resultados obtidos seriam semelhantes se tivéssemos escolhido outros navegadores. Assim, focaremos não na comparação de velocidades entre duas diferentes engines de navegadores, mas na comparação de velocidades entre duas diferentes versões de um código fonte em cada navegador (uma versão usando um valor primitivo para a string e a outra um objeto String). Complementando investigaremos também as velocidades de um mesmo caso em diferentes versões do mesmo navegador. Os resultados dos exemplos de análise de desempenho foram coletados na mesma máquina e então testados em outras máquinas com diferente OS/hardware com o objetivo de validar as velocidades aferidas.
Cenário
Para as análises de desempenho a proposta é simples: declaramos duas variáveis com o mesmo valor uma como valor primitivo para string e a outra como objeto String:
var stringprimitivo = "Hello"; var stringobjeto = new String("Hello");
e a seguir manipulamos ambas igualmente:
1. Propriedade length
var i = stringprimitivo.length; var k = stringobjeto.length;
Se presupormos que durante o tempo de execução o objeto provisório criado para o valor primitivo da string, stringprimitivo for tratado, em termos de performance de maneira igual ao objeto String stringobjeto pela engine JavaScript então seria de se esperar a mesma latência nos dois casos de acesso à propriedade length
. Contudo, como se pode observar no gráfico de barras mostrado a seguir, acessar a propriedade length
com uso de valor primitivo da string stringprimitivo é muito mais rápido do que acessar com uso do Objeto String stringobjeto.
(Ver esta análise de desempenho em jsPerf)
Atualmente no navegador Chrome 24.0.1285 executar stringprimitivo.length
é 2.5x mais rápido do que executar stringobjeto.length
e no navegador Firefox 17 é cerca de 2x mais rápido (com mais operações por segundo). Consequentemente podemos concluir que a engine JavaScript dos navegadores se valem de caminhos mais curtos, porém diferentes, para acessar a propriedade length
quando lidam com valores primitivos.
Na engine JS SpiderMonkey, por exemplo, a propriedade length
padrão é implementada como uso de um método get()
e mais especificamente com o pseudo-código para manipular a operação get
como mostrado a seguir:
// retorno direto da propriedade "length" if (typeof(value) == "string" && property == "length") { return StringLength(value); } // retorno geral de propriedades object = ToObject(value); return InternalGetProperty(object, property);
Assim, quando você inspeciona a propriedade length
em uma string definida como valor primitivo a engine JS retorna de imediato o tamanho (length) da string, sem realizar uma busca por todas as propriedades e sem necessidade de criar um objeto String provisório. A não ser que você adicione uma propriedade ou método à String.prototype
e use a palavra-chave |this|, como mostrado a seguir:
String.prototype.getThis = function () { return this; } console.log("hello".getThis());
Dessa forma um objeto não será criado quando se acessa os métodos de String.prototype
, como por exemplo: String.prototype.valueOf()
. Cada engine JS prevê internamente uma otimização similar a essa com o objetivo de produzir resultados mais rapidamente.
2. Método charAt()
var i = stringprimitivo.charAt(0); var k = stringobjeto["0"];
(Ver esta análise de desempenho — charAt() em jsPerf)
Essa análise de desempenho mostra claramente que inspecionar o valor da primeira string no Firefox 20 é substancialmente mais rápido em stringprimitivo do que em stringobjeto, cerca de 70x de aumento da performance. Para outros navegadores o resultado são semelhante embora com velocidades diferentes. Notar as diferenças entre as versões incrementais do navegador Firefox. Isso é uma indicação de como pequenas variações no código podem afetar a velocidade da execução para determinadas chamadas.
3. Método indexOf()
var i = stringprimitivo.indexOf("e"); var k = stringobjeto.indexOf("e");
(Ver esta análise de desempenho — IndexOf() em jsPerf)
De modo semelhante, para esse caso, podemos notar que o valor da string declarado em stringprimitivo pode ser usado em um número bem maior de operações em relação a stringobjeto. E mais, a engine JS dos navegadores em suas versões incrementais produzem medidas diferentes.
4. Método match()
Para os métodos a seguir os resultados são semelhantes e assim fornecemos apenas o link para a página de análise de desempenho.
(Ver esta análise de desempenho — match() em jsPerf)
5. Método replace()
(Ver esta análise de desempenho —replace() em jsPerf)
6. Método toUpperCase()
(Ver esta análise de desempenho — toUpperCase() em jsPerf)
7. Método valueOf()
var i = stringprimitivo.valueOf(); var k = stringobjeto.valueOf();
A partir daqui os testes começam a ficar mais interessantes. O que será que acontece quando aplicamos o mais comum dos métodos de uma string: valueOf()
? Parece que a maioria dos navegadores possuem um mecanismo para determinar se a string é um valor primitivo ou um objeto e então usam o caminho mais rápido para retornar o resultado. Surpreendentemente o navegador Firefox versões 20 e maiores, para esse método, tratam o objeto String stringobjeto, com uma velocidade7x superior.
(Ver esta análise de desempenho —valueOf() em jsPerf)
Convém mencionar que o navegador Chrome 22.0.1229 também prefere o objeto String, sendo que para a versão 23.0.1271 foi implementado um novo mecanismo para retornar conteúdos de strings como valor primitivo.
Uma maneira simples de rodar essa análise de performance no console do seu navegador está descrita na área de comentários da página disponibilizada em jsperf.
8. Adicionando duas strings
var i = stringprimitivo + " there"; var k = stringobjeto + " there";
(Ver esta análise de desempenho —get str value em jsPerf)
A seguir vamos adicionar duas strings sendo uma delas um valor primitivo. Conforme mostrado no gráfico a seguir, tanto no navegador Firefox como no navegador Chrome há um aumento de velocidade de 2.8x e 2x respectivamente em favor de stringprimitivo quando comparado com stringobjeto.
9. Adicionando duas strings com valueOf()
var i = stringprimitivo.valueOf() + " there"; var k = stringobjeto.valueOf() + " there";
(Ver esta análise de desempenho —str valueOf em jsPerf)
Nesse caso podemos observar que novamente o navegador Firefox favorece stringobjeto.valueOf()
uma vez que para stringprimitivo.valueOf()
há necessidade de manipular a árvore de heranças e consequentemente criar um objeto provisório para stringprimitivo. O efeito que esse encadeamento de eventos exerce sobre a performance pode ser observado no exemplo a seguir.
10. Objeto provisório for-in
var i = ""; for (var temp in stringprimitivo) { i += stringprimitivo[temp]; } var k = ""; for (var temp in stringobjeto) { k += stringobjeto[temp]; }
Nesse caso o valor da string é definido incrementalmente em um loop. No loop for-in
a expressão a ser avaliada é normalmente um objeto, contudo se ela for um valor primitivo esse malor será transformado em objeto provisório por coerção. Sem dúvida esse método não é apropriado para se inspecionar o valor de uma string, mas é uma das várias maneiras que resultam na criação de um objeto provisório e por isso convém ser analisado.
(Ver esta análise de desempenho —Properties em jsPerf)
Como era de se esperar o navegador Chrome favorece o valor stringprimitivo ao passo que os navegadores Firefox e Safari favorecem o objeto stringobjeto. Caso isso pareça típico, passemos ao último teste.
11. Adicionando duas strings a um objeto String
var str3 = new String(" there"); var i = stringprimitivo + str3; var k = stringobjeto + str3;
(Ver esta análise de desempenho —2 str values em jsPerf)
Nos exemplos anteriores vimos que a performance é melhor no navegador Firefox em diferentes versões quando um valor inicial da string é um objeto String, tal como stringobjeto e assim seria normal esperar o mesmo ao adicionar stringobjeto a outro objeto String, o que basicamente é a mesma coisa. Contudo, convém notar que adicionar uma string a um objeto String é mais rápido, no navegador Firefox quando usamos um valor stringprimitivo em lugar de um stringobjeto. Isso prova uma vez mais como pequenas variações no código, tal como caminhos para bugs, conduz a diferentes números em uma análise de performance.
Conclusão
Baseado nas análises de desempenho descritas nessa matéria é possível observar como pequenos detalhes relacionados à declaração de valores de strings podem afetar a performance. É recomendável que você continue declarando suas strings da maneira como tem feito a não ser que exista uma razão muito específica para defini-las criando instâncias do objeto String. Convém lembrar que a performance geral do navegador, particularmente quando manipulando o DOM, não depende somente da performance JS na página, há uma série de outros fatores envolvidos.
Desenvolvimento com Padrões Web? Adquira os livros do Maujor
Visite o site dos livros.
Esta matéria foi publicada em: 2012-12-6 (quinta-feira). Subscreva o feed RSS 2.0 para comentários.
Comente abaixo, ou link para https://www.maujor.com/blog/2012/12/06/objetos-string-da-javascript-e-performance/trackback no seu site.