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

Доступ к локальной переменной не повышает производительность

**** Разъяснение **: Я не ищу самый быстрый код или оптимизацию. Я хотел бы понять, почему некоторый код, который, кажется, не оптимизирован или не оптимален, фактически работает в целом последовательно быстрее.

Краткая версия

Почему этот код:

var index = (Math.floor(y / scale) * img.width + Math.floor(x / scale)) * 4;

Более эффективный, чем этот?

var index = Math.floor(ref_index) * 4;

Длинная версия

На этой неделе автор Impact js опубликовал статью о некоторой проблеме рендеринга:

http://www.phoboslab.org/log/2012/09/drawing-pixels-is-hard

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

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

У меня есть тестовая страница здесь:

http://www.mx981.com/stuff/resize_bench/test.html

jsPerf: http://jsperf.com/local-variable-due-to-the-scope-lookup

Чтобы запустить тест, щелкните изображение, и результаты появятся на консоли.

Существуют три разные версии:

Исходный код:

for( var y = 0; y < heightScaled; y++ ) {
    for( var x = 0; x < widthScaled; x++ ) {
        var index = (Math.floor(y / scale) * img.width + Math.floor(x / scale)) * 4;
        var indexScaled = (y * widthScaled + x) * 4;
        scaledPixels.data[ indexScaled ] = origPixels.data[ index ];
        scaledPixels.data[ indexScaled+1 ] = origPixels.data[ index+1 ];
        scaledPixels.data[ indexScaled+2 ] = origPixels.data[ index+2 ];
        scaledPixels.data[ indexScaled+3 ] = origPixels.data[ index+3 ];
    }
}

jsPerf: http://jsperf.com/so-accessing-local-variable-doesn-t-improve-performance

Одна из моих попыток оптимизировать его:

var ref_index = 0;
var ref_indexScaled = 0
var ref_step = 1 / scale;
for( var y = 0; y < heightScaled; y++ ) {
    for( var x = 0; x < widthScaled; x++ ) {
        var index = Math.floor(ref_index) * 4;
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+1 ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+2 ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+3 ];

        ref_index+= ref_step;
    }
}

jsPerf: http://jsperf.com/so-accessing-local-variable-doesn-t-improve-performance

Тот же оптимизированный код, но с пересчетом индексной переменной каждый раз (Hybrid)

var ref_index = 0;
var ref_indexScaled = 0
var ref_step = 1 / scale;
for( var y = 0; y < heightScaled; y++ ) {
    for( var x = 0; x < widthScaled; x++ ) {
        var index = (Math.floor(y / scale) * img.width + Math.floor(x / scale)) * 4;
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+1 ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+2 ];
        scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ index+3 ];

        ref_index+= ref_step;
    }
}

jsPerf: http://jsperf.com/so-accessing-local-variable-doesn-t-improve-performance

Единственное различие в двух последних - это вычисление переменной "index". И, к моему удивлению, оптимизированная версия в большинстве браузеров медленнее (кроме оперы).

Результаты личного тестирования (а не тесты jsPerf):

  • Opera

    Original:  8668ms
    Optimized:  932ms
    Hybrid:    8696ms
    
  • хром

    Original:  139ms
    Optimized: 145ms
    Hybrid:    136ms
    
  • Safari

    Original:  433ms
    Optimized: 853ms
    Hybrid:    451ms
    
  • Firefox

    Original:  343ms
    Optimized: 422ms
    Hybrid:    350ms
    

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

Итак, почему "оптимизированная" версия медленнее?

Я думал, что это может быть из-за того, что какой-то движок JavaScript не оптимизирует оптимизированную версию, потому что он не достаточно горячий, но после использования --trace-opt в хром кажется, что вся версия правильно скомпилирована V8.

В этот момент я немного невежественный и удивляюсь, если кто-нибудь узнает, что происходит?

Я также сделал еще несколько тестовых примеров на этой странице:

http://www.mx981.com/stuff/resize_bench/index.html

4b9b3361

Ответ 1

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

Добавление следующего 4-го теста в http://www.mx981.com/stuff/resize_bench/test.html

// Test 4
console.log('- p01 -');
start = new Date().getTime();
for (i=0; i<nbloop; i++) {
  var index = 0;
  var ref_indexScaled = 0
  var ref_step=1/scale;


  for( var y = 0; y < heightScaled; y++ ) {
    for( var x = 0; x < widthScaled; x++ ) {
      var z= index<<2;
      scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ z++ ];
      scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ z++ ];
      scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ z++ ];
      scaledPixels.data[ ref_indexScaled++ ] = origPixels.data[ z++ ];

      index+= ref_step;
    }
  }
}
end = new Date().getTime();
console.log((end-start)+'ms');

Устанавливает следующие числа в Opera Далее:

  • Оригинал - 2311мс
  • refactor - 112ms
  • hybrid - 2371ms
  • p01 - 112ms

Ответ 2

Используя некоторые базовые методы, вы можете очень оптимизировать производительность:

  • При запуске нескольких циклов в циклах используйте:

    while (i--) {   /* некоторый код здесь */ }

... где я - значение больше 0.

  • Кэширование переменных/локализация переменных для минимизации вычислений. Для больших вычислений это означает, что часть вычисления вычисляется на правом слое абстракции.

  • Повторное использование переменных (переназначение начальной инициализации может стать проблемой для больших объемов обработки данных). ПРИМЕЧАНИЕ. Это плохой принцип проектирования программ, но отличный принцип производительности!

  • Уменьшить глубину свойства. Использование object.property убивает производительность по сравнению с var, содержащим "object_propertyvalue".

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

function resize_Test5( img, scale ) {
    // Takes an image and a scaling factor and returns the scaled image

    // The original image is drawn into an offscreen canvas of the same size
    // and copied, pixel by pixel into another offscreen canvas with the 
    // new size.

    var widthScaled = img.width * scale;
    var heightScaled = img.height * scale;

    var orig = document.createElement('canvas');
    orig.width = img.width;
    orig.height = img.height;
    var origCtx = orig.getContext('2d');
    origCtx.drawImage(img, 0, 0);
    var origPixels = origCtx.getImageData(0, 0, img.width, img.height);

    var scaled = document.createElement('canvas');
    scaled.width = widthScaled;
    scaled.height = heightScaled;
    var scaledCtx = scaled.getContext('2d');
    var scaledPixels = scaledCtx.getImageData( 0, 0, widthScaled, heightScaled );

    // optimization start
    var old_list = origPixels.data;
    var image_width = img.width;
    var h = heightScaled;
    var w = widthScaled;
    var index_old;
    var index_new;
    var h_scale;
    var new_list = [];
    var pre_index_new;

    while(h--){
        h_scale = Math.floor(h / scale) * image_width;
        pre_index_new = h * widthScaled;
        while(w--){
            index_old = (h_scale + Math.floor(w / scale)) * 4;
            index_new = (pre_index_new + w) * 4;
            new_list[ index_new ]     = old_list[ index_old ];
            new_list[ index_new + 1 ] = old_list[ index_old + 1 ];
            new_list[ index_new + 2 ] = old_list[ index_old + 2 ];
            new_list[ index_new + 3 ] = old_list[ index_old + 3 ];
        }
    }
    scaledPixels.data = new_list;
    // optimization stop

    scaledCtx.putImageData( scaledPixels, 0, 0 );
    return scaled;
}