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

    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 даже занял первое место.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 29

      0
      А где вы взяли firefox 44? FF43 только сегодня должен стать Developer Edition. Или я чего то не понимаю?
        0
        Это ночная сборка, только сегодня версия сменилась.
        +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)
        
          0
          Неа, в хроме 45.0.2454.93 m на win7 не быстрее
          jsfiddle.net/rf7co2gj

          результаты у меня

          regex: 472.494ms
          nativeSlice: 598.145ms
            0
            Чудеса)

            У меня:
            Хром:
            
            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
            
              +1
              В Хроме и особенно в IE скорость выполнения может сильно меняться в зависимости от того, открыты ли инструменты разработчика. Вероятно, какие-то оптимизации отключаются. FF в этом вроде бы не замечен (ну, во всяком случае, чтобы сильно).
              0
              Добавил в конец поста данные по вашему варианту.
              +2
              Наивная сборка строки через числа работает быстрее наивной сборки через slice:
              function mathPower (num){
                  if (num < 999)
                      return num;
                  return mathPower(~~(num / 1000)) + "'"
                         + ("00"+(~~(num % 1000))).substr(-3,3);
              }
              

              jsfiddle.net/5nzL3yLo
                0
                Спасибо. Попробовал в 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
                

                  0
                  Обновил конец статьи, добавил ваш вариант в тесты. У меня он выиграл на IE.
                +3
                … если вы знаете нужную локаль заранее, лучше воспользоваться заменой по регулярному выражению…

                Вы же сами пишете выше про конструктор. Да, как только вам стала известна локаль, то для Intl лучше предварительно создавать эм… форматтер
                new Intl.NumberFormat([locales[, options]])
                

                В таком случае он работает достаточно быстро. А регулярки не обеспечат вам поддержку всех локалей, Китайские, японские цифры или иврит все вам поломают, намучаетесь.
                  0
                  (пардон, не туда)
                    0
                    Да, пожалуй, если сложный случай, тем более — если дело не только в разделении на группы, лучше положиться на проверенные стандарты. Но если случай простой, всего лишь разделить запятыми или пробелами, и важна скорость, то можно, наверное, и регуляркой ограничиться.
                    +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);
                    
                      0
                      То есть вы полагаетесь на длину 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 ]
                      
                      

                        0
                        Добавил в конец поста данные по вашему варианту.
                        +1
                        function 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;
                        }
                        
                          +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
                          


                            +1
                            А Вы на лэптопе тестируете? Посмотрите в настройках Advanced Power Settings-> Internet Explorer -> Javascript Timer Frequency, там должно быть Maximum Perfomance
                              0
                              Да, но он работает от сети. Проверил, там при работе от сети стоит «Максимальная производительность».
                                0
                                Обновил конец статьи, добавил ваш вариант в тестовую страницу. У меня он выиграл на Firefox.
                            +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;
                            }
                            
                              0
                              Спасибо. Новая табличка. В Хроме более чем в два раза быстрее предыдущего, в IE чуть медленнее, а вот в Firefox почему-то более чем в два раза медленнее по сравнению с fmt1:

                              Скриншоты:





                              0
                              Плюс самый разжёванный вариант (см. P.S. 4 в конце статьи).
                                +1
                                Нет смысла заново инициализировать регулярку. Так же быстрее:

                                var pattern = new RegExp('\B(?=(?:\d{3})+$)', 'g');
                                
                                i.toString().replace( pattern , ',' ); //А это в цикле
                                
                                
                                  +1
                                  upd: Прошлый комментарий отменяется. Увидел в тестах.
                                    0
                                    Всё равно спасибо)
                                  +1
                                  и он оказался довольно быстрым, а в тестах на Firefox даже занял первое место.
                                  Я вижу, что во всех браузерах, кроме IE11, slice_concatenate имеет наибольшую скорость работы. exhaustion на 3-5 местах. Может, я не туда смотрю?
                                    0
                                    У них какой-то баг на сайте. Кажется, ни один из моих тестов на Firefox Nightly 44.0a1 не сохранился и не отображается в сведённых чартах, хотя после каждого теста в текущей табличке всё отображается. Может, при распознавании ночных сборок у них сбой происходит.

                                  Only users with full accounts can post comments. Log in, please.