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
.Сначала скрипт определяет переменные и для иллюстрации выводит форматирование всеми способами (пять идентичных результатов). Затем следуют пять тестовых циклов с отображением прошедшего времени после каждого.
'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 даже занял первое место.
Комментарии (29)
mwizard
22.09.2015 09:20+2Наивная сборка строки через slice работает быстрее регулярок:
var s = i.toString(); var o = []; var l = s.length; var off = l % 3; var groups = Math.floor(l / 3); var x; if (off) { o.push(s.slice(0, off)); } for (x = 0; x < groups; x++) { o.push(s.slice(off, off += 3)); } var result = o.join("'"); console.log(result)
NeXTs_od
22.09.2015 12:12Неа, в хроме 45.0.2454.93 m на win7 не быстрее
jsfiddle.net/rf7co2gj
результаты у меня
regex: 472.494ms
nativeSlice: 598.145msvmb
22.09.2015 14:58Чудеса)
У меня:Хром: regex: 985.489ms nativeSlice: 393.228ms regex: 1030.108ms nativeSlice: 398.542ms regex: 1016.810ms nativeSlice: 412.375ms Firefox: regex: 608.23ms nativeSlice: 999.5ms regex: 573.7ms nativeSlice: 898.71ms regex: 613.18ms nativeSlice: 933.68ms IE: regex: 1 766,432 мс nativeSlice: 1 702,35 мс regex: 1 651,024 мс nativeSlice: 1 684,765 мс regex: 1 760,125 мс nativeSlice: 1 690,707 мс Node.js: regex: 667ms nativeSlice: 374ms regex: 748ms nativeSlice: 392ms regex: 764ms nativeSlice: 380ms
dom1n1k
22.09.2015 19:36+1В Хроме и особенно в IE скорость выполнения может сильно меняться в зависимости от того, открыты ли инструменты разработчика. Вероятно, какие-то оптимизации отключаются. FF в этом вроде бы не замечен (ну, во всяком случае, чтобы сильно).
andruekonst
22.09.2015 22:16+2Наивная сборка строки через числа работает быстрее наивной сборки через slice:
function mathPower (num){ if (num < 999) return num; return mathPower(~~(num / 1000)) + "'" + ("00"+(~~(num % 1000))).substr(-3,3); }
jsfiddle.net/5nzL3yLovmb
22.09.2015 23:20Спасибо. Попробовал в Node.js и консолях:
результат регулярки плюс по три результата вашего вариантаNode.Js [ 0, 64840650 ] [ 0, 16904683 ] [ 0, 14002223 ] [ 0, 15718417 ] Chrome 80 102 101 98 Firefox 218 214 218 239 IE 215 193 204 186
ua9msn
22.09.2015 10:34+3… если вы знаете нужную локаль заранее, лучше воспользоваться заменой по регулярному выражению…
Вы же сами пишете выше про конструктор. Да, как только вам стала известна локаль, то для Intl лучше предварительно создавать эм… форматтер
new Intl.NumberFormat([locales[, options]])
В таком случае он работает достаточно быстро. А регулярки не обеспечат вам поддержку всех локалей, Китайские, японские цифры или иврит все вам поломают, намучаетесь.vmb
22.09.2015 15:05Да, пожалуй, если сложный случай, тем более — если дело не только в разделении на группы, лучше положиться на проверенные стандарты. Но если случай простой, всего лишь разделить запятыми или пробелами, и важна скорость, то можно, наверное, и регуляркой ограничиться.
mwizard
22.09.2015 14:00+1Вот этот способ в пять раз быстрее регулярки:
var s = i.toString(); var l = s.length; var result = ''; if (l > 15) result += s.slice(-18, -15) + "'"; if (l > 12) result += s.slice(-15, -12) + "'"; if (l > 9) result += s.slice(-12, -9) + "'"; if (l > 6) result += s.slice(-9, -6) + "'"; if (l > 3) result += s.slice(-6, -3) + "'"; result += s.slice(-3);
vmb
22.09.2015 15:25То есть вы полагаетесь на длину
Number.MAX_SAFE_INTEGER
? Интересное практичное упрощение.
У меня (небольшой прирост только в Node.js):
Регулярка vs по три теста вашего способаХром: 80 99 97 99 Firefox: 218 599 633 656 IE: 215 377 370 358 Node.js: [ 0, 64840650 ] [ 0, 46075947 ] [ 0, 53800444 ] [ 0, 43452631 ]
3axap4eHko
23.09.2015 01:07+1function fmt(n) { n = n.toString(); const l = n.length; var s='', i=0; while(i<l){ if((l-i)%3 || !i){ s+=n[i++]; }else{ s+=','+n[i++]; } } return s; }
vmb
23.09.2015 01:23+1Спасибо!
Проверил, вроде бы по сравнению с регуляркой получается быстрее везде, кроме IE. Вот результаты — по три запуска, сначала регулярка, потом ваш вариант.
РезультатыNode.Js [ 0, 72634428 ] [ 0, 35303136 ] [ 0, 73739665 ] [ 0, 38605069 ] [ 0, 85149363 ] [ 0, 39749604 ] Chrome 98 36 119 36 91 34 Firefox 214 185 224 186 220 182 IE 186 266 175 277 185 302
3axap4eHko
23.09.2015 01:24+1А Вы на лэптопе тестируете? Посмотрите в настройках Advanced Power Settings-> Internet Explorer -> Javascript Timer Frequency, там должно быть Maximum Perfomance
vmb
23.09.2015 01:33Да, но он работает от сети. Проверил, там при работе от сети стоит «Максимальная производительность».
vmb
23.09.2015 01:59Обновил конец статьи, добавил ваш вариант в тестовую страницу. У меня он выиграл на Firefox.
3axap4eHko
23.09.2015 03:45+1И еще
function fmt2(n) { var s=''; while(n){ s=('000'+n%1000).slice(n>=1000 ? -3 : -n.toString().length)+s; n=n/1000|0; if(n) { s=','+s; } } return s; }
vmb
23.09.2015 04:33Спасибо. Новая табличка. В Хроме более чем в два раза быстрее предыдущего, в IE чуть медленнее, а вот в Firefox почему-то более чем в два раза медленнее по сравнению с fmt1:
Скриншоты:
AHDPEu
24.09.2015 09:44+1Нет смысла заново инициализировать регулярку. Так же быстрее:
var pattern = new RegExp('\B(?=(?:\d{3})+$)', 'g'); i.toString().replace( pattern , ',' ); //А это в цикле
mwizard
24.09.2015 15:18+1и он оказался довольно быстрым, а в тестах на Firefox даже занял первое место.
Я вижу, что во всех браузерах, кроме IE11,slice_concatenate
имеет наибольшую скорость работы.exhaustion
на 3-5 местах. Может, я не туда смотрю?vmb
24.09.2015 16:10У них какой-то баг на сайте. Кажется, ни один из моих тестов на Firefox Nightly 44.0a1 не сохранился и не отображается в сведённых чартах, хотя после каждого теста в текущей табличке всё отображается. Может, при распознавании ночных сборок у них сбой происходит.
Yahweh
А где вы взяли firefox 44? FF43 только сегодня должен стать Developer Edition. Или я чего то не понимаю?
vmb
Это ночная сборка, только сегодня версия сменилась.