Скорость локального форматирования числа

от автора


I. Задача

В одном из скриптов столкнулся с необходимостью отформатировать вывод больших чисел для удобства чтения. Нашёл несколько рецептов, как разделить число на группы по разряду, но в обсуждениях высказывались сомнения в производительности. Решил провести несколько тестов.

II. Варианты решения

Предположим, у нас есть переменная с числом.

    var i = 100000; 

1. Способ замены по регулярным выражениям

Существует несколько вариантов, этот мне показался наиболее компактным:

    i.toString().replace( /\B(?=(?:\d{3})+$)/g, ',' ); 

2. При помощи объекта Intl

А именно метода format конструктора NumberFormat. Возможны два варианта.

а. С умолчанием:

    var fn_undef = new Intl.NumberFormat();     fn_undef.format(i); 

б. С принудительным заданием локали:

    var fn_en_US = new Intl.NumberFormat('en-US');     fn_en_US.format(i); 

3. При помощи toLocaleString

У этого способа много общего с предыдущим, как можно понять из описаний. Тоже рассмотрим два варианта.

а. С умолчанием:

    i.toLocaleString(); 

б. С принудительным заданием локали:

    i.toLocaleString('en-US'); 

III. Тесты

Представим, что нам нужно будет вывести таблицу с большим количеством отформатированных чисел — в таком случае разница в быстродействии способов будет важна. Я попробовал протестировать цикл с сотней тысяч операций на нескольких движках и в разных средах исполнения.

1. Node.js 4.1.0

К сожалению, локаль ru-RU в этой версии Node.js не поддерживается (или я не знаю, как добавить её поддержку), поэтому для единообразия пришлось везде использовать локаль en-US.

Сначала скрипт определяет переменные и для иллюстрации выводит форматирование всеми способами (пять идентичных результатов). Затем следуют пять тестовых циклов с выводом времени после каждого.

Код для Node.js

'use strict';  var i = 100000; const fn_undef = new Intl.NumberFormat(); const fn_en_US = new Intl.NumberFormat('en-US');  console.log( i.toString().replace( /\B(?=(?:\d{3})+$)/g, ',' ) ); console.log( fn_undef.format(i)                                ); console.log( fn_en_US.format(i)                                ); console.log( i.toLocaleString()                                ); console.log( i.toLocaleString('en-US')                         );  var time = process.hrtime(); while (i-- > 0) { 	i.toString().replace( /\B(?=(?:\d{3})+$)/g, ',' ); } console.log(process.hrtime(time));  i = 100000; time = process.hrtime(); while (i-- > 0) { 	fn_undef.format(i); } console.log(process.hrtime(time));  i = 100000; time = process.hrtime(); while (i-- > 0) { 	fn_en_US.format(i); } console.log(process.hrtime(time));  i = 100000; time = process.hrtime(); while (i-- > 0) { 	i.toLocaleString(); } console.log(process.hrtime(time));  i = 100000; time = process.hrtime(); while (i-- > 0) { 	i.toLocaleString('en-US'); } console.log(process.hrtime(time)); 

Функция для профайлинга hrtime выдаёт разницу во времени как кортеж из двух чисел в массиве: количество секунд и наносекунд.

Обобщённый пример вывода скрипта (исключая начальные иллюстрации способов):

[  0,  64840650 ] [  0, 473762595 ] [  0, 470775460 ] [  0, 514655925 ] [ 14, 120328524 ] 

Как мы видим, первый вариант самый быстрый. Следующие два почти не отличаются друг от друга, но медленнее первого на порядок. Четвёртый способ ещё чуть медленнее. Но последний оказывается аномально медленным.

Тут мы и видим самую существенную разницу между методом Intl.NumberFormat.format и Number.toLocaleString: в первом мы один раз задаём локаль в конструкторе, во втором мы задаём её в каждом вызове. При определении локали интерпретатор производит довольно ресурсоёмкие операции, описанные в справке. В первом случае он проивзодит их раз и на всё время работы форматера, во втором случае он производит их заново сто тысяч раз. Малозаметная разница в коде, но очень существенная для времени выполнения.

Можно сделать предварительный вывод: если вы знаете нужную локаль заранее, лучше воспользоваться заменой по регулярному выражению. Если локаль непредсказуема, лучше пользоваться методом Intl.NumberFormat.format, не задавая локаль принудительно.

Если протестировать данный код (заменив функцию профайлинга) в браузерах, мы убедимся, что и для них этот вывод правильный.

2. Браузеры

Запустим этот код в консолях.

Код для браузеров

var i = 100000; const fn_undef = new Intl.NumberFormat(); const fn_en_US = new Intl.NumberFormat('en-US');  console.log( i.toString().replace( /\B(?=(?:\d{3})+$)/g, ',' ) ); console.log( fn_undef.format(i)                                ); console.log( fn_en_US.format(i)                                ); console.log( i.toLocaleString()                                ); console.log( i.toLocaleString('en-US')                         );  var time = Date.now(); while (i-- > 0) { 	i.toString().replace( /\B(?=(?:\d{3})+$)/g, ',' ); } console.log(Date.now() - time);  i = 100000; time = Date.now(); while (i-- > 0) { 	fn_undef.format(i); } console.log(Date.now() - time);  i = 100000; time = Date.now(); while (i-- > 0) { 	fn_en_US.format(i); } console.log(Date.now() - time);  i = 100000; time = Date.now(); while (i-- > 0) { 	i.toLocaleString(); } console.log(Date.now() - time);  i = 100000; time = Date.now(); while (i-- > 0) { 	i.toLocaleString('en-US'); } console.log(Date.now() - time); 

Теперь сравнивать придётся миллисекунды, но и это будет достаточно наглядным.

а. Chrome 47.0.2515.0

   80   543   528   604 18699 

б. Firefox 44.0a1

 218  724  730  439 7177 

в. IE 11.0.14

  215   328   355 32628 37384 

Видим, что Chrome в последнем способе отстал от Node.js, Firefox оказался в этом же проблемном месте в два раза быстрее, а в IE 11 предпоследний способ по скорости значительно приблизился к последнему (т. е. опущение локали мало чем спасает этот вариант в IE).

Наконец, для удобства желающих проверить самому и для большей объективности, добавил страничку на jsperf.com. У меня последняя редакция тестов выдала следующее:

Код там упрощённый, потому что основную работу по прогону циклов сайт берёт на себя. Можете поэкспериментировать, редактируя код и добавляя свои варианты тестов.

ссылка на оригинал статьи http://habrahabr.ru/post/267387/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *