Как стать автором
Обновить

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

Время на прочтение 5 мин
Количество просмотров 7.5K

I. Задача



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

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



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

    var i = 100000;


Превратить её вывод в 100,000 (или в 100 000, или в 100.000) можно следующими способами.

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. При помощи метода Number.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. У меня последняя редакция тестов выдала следующее:

Скриншоты







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

P.S. В комментариях добавили ещё два способа. Они, хоть и существенно объёмнее по коду, во многих тестовых случаях ещё быстрее замены по регулярному выражению (тесты на Node и в консолях браузеров: раз, два). Добавил тестовую страничку со всеми семью способами. У меня она выдаёт:

Скриншоты








P.S. 2 Появились ещё две функции, сделал новые тесты (раз, два) и добавил их на jsperf.com. Заодно чуть поправил код с регулярным выражением, вынеся компиляцию из цикла: хоть на MDN и говорится, что в циклах литеральные регулярные выражения не перекомпилируются, я не уверен, имеется ли в виду — когда они определяются вне цикла или даже когда внутри (в Perl ест дополнительный флаг, запрещающий перекомпилирование не изменяющегося в цикле регулярного выражения, не знаю, как себя ведёт в этих случаях JS). Во всяком случае, тесты в Node.js и браузерах показали небольшой прирост скорости при вынесении регулярки из цикла. По итогам новых тестов из девяти способов однозначно выигрывают новые четыре, «математические», но при этом в каждом браузере выигрывают разные «математические» способы. Мои новые результаты:

Скриншоты







P.S. 3 Ещё +1 функция: новая таблица (уже десять вариантов), мои показатели.

P.S. 4 Решил добавить самый линейный вариант — перебор всех возможных длин целого числа в безопасном диапазоне Number.MAX_SAFE_INTEGER c конкатенацией строки посимвольно и вставкой в нужных местах разделителя. Это уже одиннадцатый вариант (функция exhaustion() ), и он оказался довольно быстрым, а в тестах на Firefox даже занял первое место.
Теги:
Хабы:
+8
Комментарии 29
Комментарии Комментарии 29

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн