Подтвердить что ты не робот

Длительность выполнения задач JavaScript

Я заметил на днях вопрос (Уменьшение использования процессора Javascript), и я был заинтригован.

По сути, парень хотел зашифровать некоторые файлы по характеру. Очевидно, что все это за один раз закроет браузер.

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

Теперь я действительно не хочу вдаваться в то, почему это, вероятно, не очень хорошая идея в javascript. Но я хотел посмотреть, могу ли я придумать решение.

Я вспомнил, как смотрел видео Douglas Crockford в js conf. Видео было связано с node.js и циклом событий. Но я вспомнил, что он говорил о том, чтобы разбить длинные функции вниз на отдельные куски, поэтому вновь вызываемая функция переходит в конец цикла событий. Вместо того, чтобы забивать цикл событий с помощью длительной задачи, предотвращая что-либо еще.

Я знал, что это решение, достойное моего расследования. Будучи разработчиком интерфейсов, я никогда не сталкивался с очень большими задачами в JS и очень хотел узнать, как их разбить и как они работают.

Я решил попробовать рекурсивную функцию, которая вызывает себя внутри setTimeout 0ms. Я понял, что это обеспечит перерывы в цикле событий для чего-то еще, что должно было произойти во время его запуска. Но я также подумал, что пока ничего не происходит, вы получите максимальный расчет.

Вот что я придумал.

(Я собираюсь извиниться за код. Я экспериментировал в консоли, так что это было быстро и грязно.)

function test(i, ar, callback, start){
    if ( ar === undefined ){
        var ar = [],
        start = new Date;
    };
    if ( ar.length < i ){
        ar.push( i - ( i - ar.length )  );
        setTimeout(function(){
            test( i, ar, callback, start);
        },0);
    }
    else {
        callback(ar, start);
    };
}

(Вы можете вставить этот код в консоль, и он будет работать)

По сути, что делает функция, она принимает число, создает массив и вызывает себя, а array.length < number нажимает счетчик до сих пор в массив. Он передает массив, созданный при первом вызове всех последующих вызовов.

Я тестировал его и, похоже, работал точно так, как предполагалось. Только его производительность довольно бедна. Я проверил его с помощью.

(опять же это не сексуальный код)

test(5000, undefined, function(ar, start ){ 
    var finish = new Date; 
    console.log(
        ar.length,
        'timeTaken: ', finish - start 
    ); 
});

Теперь я, очевидно, хотел узнать, сколько времени потребовалось для завершения, приведенный выше код занял около 20 секунд. Теперь мне кажется, что для JS не должно занимать 20 секунд, чтобы рассчитывать до 5000. Добавьте в то, что он выполняет некоторые вычисления и обработку для ввода элементов в массив. Но все же 20-е годы немного круты.

Итак, я решил создать несколько одновременно, чтобы узнать, как это повлияло на производительность браузера и скорость вычислений.

(код не становится более сексуальным)

function foo(){ 
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
test(5000, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

Итак, пять в общей сложности работают одновременно и не вызывают зависания браузера.

после завершения процесса все результаты возвращаются практически в одно и то же время. для их завершения потребовалось около 21,5 с. Это всего на 1,5 с медленнее, чем у одного. Но я перемещал мышь вокруг окна на элементах, которые имели эффекты :hover, чтобы убедиться, что браузер все еще отвечает, так что это может отражать некоторые из служебных затрат на 1,5 с.

Так как эти функции, очевидно, работают параллельно, в браузере осталось больше вычислительного сока.

Может ли кто-нибудь объяснить, что здесь происходит, и дать подробную информацию о том, как улучшить такие функции?

Просто сойди с ума, я сделал это.

function foo(){
    var count = 100000000000000000000000000000000000000;  
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 1'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 2'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 3'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 4'  ) });
    test(count, undefined, function(ar, start ){ var finish = new Date; console.log(ar.length, 'timeTaken: ', finish - start, 'issue: 5'  ) });
};

Он работал все время, когда я писал этот пост, и все еще готов к этому. Браузер не жалуется и не висит. Я добавлю время завершения, как только оно закончится.

4b9b3361

Ответ 1

setTimeout не имеет минимальной задержки 0ms. Минимальная задержка находится в диапазоне от 5 мс до 20 мс в зависимости от браузеров.

Мое личное тестирование показывает, что setTimeout не помещает вас обратно в стек событий немедленно

Живой пример

У него есть суровая минимальная задержка до того, как он снова будет вызван

var s = new Date(),
    count = 10000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    });

doo(count, function() {
    test(10, undefined, cb);
});
  • Запуск 10000 из них в параллельном подсчете до 10 занимает 500 мс.
  • Выполнение 100 отсчетов до 10 занимает 60 мс.
  • Запуск 1 подсчета до 10 занимает 40 мс.
  • Запуск 1 счета до 100 занимает 400 мс.

Клери кажется, что каждый отдельный setTimeout должен ждать не менее 4 мс для повторного вызова. Но это горло бутылки. Индивидуальная задержка на setTimeout.

Если вы планируете использовать 100 или более из них параллельно, это будет работать.

Как мы оптимизируем это?

var s = new Date(),
    count = 100,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(10, array, cb);
});

Настройте 100 параллельно в одном массиве. Это позволит избежать основного узкого места, которое является задержкой setTimeout.

Вышеупомянутое завершено в 2 мс.

var s = new Date(),
    count = 1000,
    cb = after(count, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(1000, array, cb);
});

Заканчивается через 7 миллисекунд

var s = new Date(),
    count = 1000,
    cb = after(1, function() {
        console.log(new Date() - s);    
    }),
    array = [];

doo(count, function() {
    test(1000000, array, cb);
});

Выполнение 1000 заданий параллельно является примерно оптимальным. Но вы начнете сталкиваться с узкими местами. Подсчет до 1 миллиона все еще занимает 4500 мс.

Ответ 2

Ваша проблема связана с накладными расходами и единицей работы. Навыки setTimeout очень высоки, а ваша единица работы ar.push очень низкая. Это старый метод оптимизации, известный как Block Processing. Вместо обработки одного UoW за звонок вам нужно обработать блок UoW. Насколько большой "блок" зависит от того, сколько времени занимает каждый UoW, и максимального количества времени, которое вы можете потратить в каждой команде setTimeout/call/iteration (до того, как пользовательский интерфейс перестанет отвечать).

function test(i, ar, callback, start){
if ( ar === undefined ){
    var ar = [],
    start = new Date;
};
if ( ar.length < i ){
    // **** process a block **** //
    for(var x=0; x<50 && ar.length<i; x++){
        ar.push( i - ( i - ar.length )  );
    }
    setTimeout(function(){
        test( i, ar, callback, start);
    },0);
}
else {
    callback(ar, start);
};
}

Вы должны обработать самый большой блок, который вы можете, не вызывая проблемы пользовательского интерфейса/производительности для пользователя. Предыдущие пробеги ~ 50 раз быстрее (размер блока).

По той же причине мы используем буфер для чтения файла, а не для чтения его по одному байту за раз.

Ответ 3

Просто гипотеза... может быть, что код настолько медленный, потому что вы строите стек рекурсии с 5000 экземплярами рекурсии? ваш вызов не является действительно рекурсивным, поскольку это происходит с помощью функции settimeout, но функция, которую вы передаете ему, является закрытием, поэтому она должна будет хранить все контексты закрытия...

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

Я не пробовал ничего с интерпретатором, но было бы интересно узнать, является ли время вычисления линейным с количеством рекурсий или нет... скажем: 100, 500, 1000, 5000 рекурсий...

Первое, что я попробовал бы в качестве обходного пути, не использует закрытие:

setTimeout(test, 0, i, ar, callback, start);

Ответ 4

На самом деле мы говорили об этом, то, что вы используете, - это рекурсивные функции, и JavaScript прямо сейчас не имеет " Рекурсивные вызовы хвоста", что означает, что интерпретатор/движок должен поддерживать фрейм стека для вызова "КАЖДЫЙ", который становится тяжелым.

Чтобы оптимизировать решение, я попытался бы превратить его в непосредственную исполняемую функцию, вызываемую в глобальной области.