Время выполнения с process.hrtime() возвращает значительно другой результат

У меня возникли проблемы с объяснением того, почему мой тест производительности возвращает значительно разные результаты по двум различным типам прогона.

Действия по воспроизведению проблемы:

practice1.generator должен генерировать файл test-data.json и записывать время выполнения алгоритма поиска в консоль. После этого practice1.performance-test читает из test-data.json и выполняет ту же самую функцию оценки по тем же данным.

Выход на моей машине последовательно похож на этот:

> node practice1.generator
Generate time: 9,307,061,368 nanoseconds
Total time using indexOf             : 7,005,750 nanoseconds
Total time using for loop            : 7,463,967 nanoseconds
Total time using binary search       : 1,741,822 nanoseconds
Total time using interpolation search: 915,532 nanoseconds

> node practice1.performance-test
Total time using indexOf             : 11,574,993 nanoseconds
Total time using for loop            : 8,765,902 nanoseconds
Total time using binary search       : 2,365,598 nanoseconds
Total time using interpolation search: 771,005 nanoseconds

Обратите внимание на разницу во времени выполнения в случае indexOf и binary search по сравнению с другими алгоритмами.

Если я повторно запускаю node practice1.generator или node practice1.performance-test, результат будет довольно непротиворечивым.

Теперь это так беспокоит, я не могу найти способ выяснить, какой результат заслуживает доверия, и почему возникают такие различия. Это вызвано различием между созданной тестовой матрицей и массивом тестов JSON.parse-d; или это вызвано process.hrtime(); или это какая-то неизвестная причина, по которой я даже не мог понять?


Обновление. Я проследил причину indexOf случая из-за JSON.parse. Внутри practice1.generator массив tests - это исходный сгенерированный массив; а в practice1.performance-test массив считывается из json файла и, вероятно, отличается от исходного массива каким-то образом.

Если внутри practice1.generator я вместо JSON.parse() добавлен новый массив из строки:

var tests2 = JSON.parse(JSON.stringify(tests));

performanceUtil.performanceTest(tests2);

Время выполнения indexOf теперь согласовано в обоих файлах.

> node practice1.generator
Generate time: 9,026,080,466 nanoseconds
Total time using indexOf             : 11,016,420 nanoseconds
Total time using for loop            : 8,534,540 nanoseconds
Total time using binary search       : 1,586,780 nanoseconds
Total time using interpolation search: 742,460 nanoseconds

> node practice1.performance-test
Total time using indexOf             : 11,423,556 nanoseconds
Total time using for loop            : 8,509,602 nanoseconds
Total time using binary search       : 2,303,099 nanoseconds
Total time using interpolation search: 718,723 nanoseconds

Итак, по крайней мере, я знаю, что indexOf работает лучше на исходном массиве и хуже на массиве JSON.parse -d. Тем не менее, я знаю только причину, не знаю почему.

Время выполнения двоичного поиска остается разным на 2 файла, последовательно занимая ~ 1,7 мс в practice1.generator (даже при использовании объекта JSON.parse -d) и ~ 2,3 мс в practice1.performance-test.


Ниже приведен тот же код, что и в сущности, для будущей справочной цели.

/*
 * performance-utils.js
 */
'use strict';

const performanceTest = function(tests){
  var tindexOf = process.hrtime();
  tests.forEach(testcase => {
    var result = testcase.input.indexOf(testcase.target);

    if(result !== testcase.output) console.log("Errr", result, testcase.output);
  });
  tindexOf = process.hrtime(tindexOf);

  var tmanual = process.hrtime();
  tests.forEach(testcase => {
    const arrLen = testcase.input.length;
    var result = -1;
    for(var i=0;i<arrLen;i++){
      if(testcase.input[i] === testcase.target){
        result = i;
        break;
      }
    }

    if(result !== testcase.output) console.log("Errr", result, testcase.output);
  });
  tmanual = process.hrtime(tmanual);

  var tbinary = process.hrtime();
  tests.forEach(testcase => {
    var max = testcase.input.length-1;
    var min = 0;
    var check, num;
    var result = -1;

    while(max => min){
      check = Math.floor((max+min)/2);
      num = testcase.input[check];

      if(num === testcase.target){
        result = check;
        break;
      }
      else if(num > testcase.target) max = check-1;
      else min = check+1;
    }

    if(result !== testcase.output) console.log("Errr", result, testcase.output);
  });
  tbinary = process.hrtime(tbinary);


  var tinterpolation = process.hrtime();
  tests.forEach(testcase => {
    var max = testcase.input.length-1;
    var min = 0;
    var result = -1;
    var check, num;

    while(max > min && testcase.target >= testcase.input[min] && testcase.target <= testcase.input[max]){
      check = min +  Math.round((max-min) * (testcase.target - testcase.input[min]) / (testcase.input[max]-testcase.input[min]));
      num = testcase.input[check];

      if(num === testcase.target){
        result = check;
        break;
      }
      else if(testcase.target > num) min = check + 1;
      else max = check - 1;
    }

    if(result === -1 && testcase.input[max] == testcase.target) result = max;

    if(result !== testcase.output) console.log("Errr", result, testcase.output);
  });
  tinterpolation = process.hrtime(tinterpolation);

  console.log(`Total time using indexOf             : ${(tindexOf[0] * 1e9 + tindexOf[1]).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} nanoseconds`);
  console.log(`Total time using for loop            : ${(tmanual[0] * 1e9 + tmanual[1]).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} nanoseconds`);
  console.log(`Total time using binary search       : ${(tbinary[0] * 1e9 + tbinary[1]).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} nanoseconds`);
  console.log(`Total time using interpolation search: ${(tinterpolation[0] * 1e9 + tinterpolation[1]).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} nanoseconds`);
}

module.exports = { performanceTest }




/*
 * practice1.generator.js
 */
'use strict';

require('util');
const performanceUtil = require('./performance-utils');
const fs = require('fs');
const path = require('path');
const outputFilePath = path.join(__dirname, process.argv[3] || 'test-data.json');

const AMOUNT_TO_GENERATE = parseInt(process.argv[2] || 1000);

// Make sure ARRAY_LENGTH_MAX < (MAX_NUMBER - MIN_NUMBER)
const ARRAY_LENGTH_MIN = 10000;
const ARRAY_LENGTH_MAX = 18000;
const MIN_NUMBER = -10000;
const MAX_NUMBER = 10000;

const candidates = Array.from(Array(MAX_NUMBER - MIN_NUMBER + 1), (item, index) => MIN_NUMBER + index);

function createNewTestcase(){
  var input = candidates.slice();
  const lengthToGenerate = Math.floor(Math.random()*(ARRAY_LENGTH_MAX - ARRAY_LENGTH_MIN + 1)) + ARRAY_LENGTH_MIN;

  while(input.length > lengthToGenerate){
    input.splice(Math.floor(Math.random()*input.length), 1);
  }

  const notfound = input.length === lengthToGenerate ?
    input.splice(Math.floor(Math.random()*input.length), 1)[0] : MIN_NUMBER-1;

  const output = Math.floor(Math.random()*(input.length+1)) - 1;
  const target = output === -1 ? notfound : input[output];

  return {
    input,
    target,
    output
  };
}

var tgen = process.hrtime();

var tests = [];
while(tests.length < AMOUNT_TO_GENERATE){
  tests.push(createNewTestcase());
}

fs.writeFileSync(outputFilePath, JSON.stringify(tests));
var tgen = process.hrtime(tgen);
console.log(`Generate time: ${(tgen[0] * 1e9 + tgen[1]).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} nanoseconds`);

performanceUtil.performanceTest(tests);



/*
 * practice1.performance-test.js
 */
'use strict';

require('util');
const performanceUtil = require('./performance-utils');
const fs = require('fs');
const path = require('path');
const outputFilePath = path.join(__dirname, process.argv[2] || 'test-data.json');

var tests = JSON.parse(fs.readFileSync(outputFilePath));
performanceUtil.performanceTest(tests);

Ответ 1

Как вы уже заметили, разница в производительности приводит к сравнению: generated array vs JSON.parse d. Что мы имеем в обоих случаях: одни и те же массивы с одинаковыми номерами? Таким образом, производительность поиска должна быть одинаковой? Нет.

Каждый механизм Javascript имеет различные структуры типов данных для представления одинаковых значений (числа, объекты, массивы и т.д.). В большинстве случаев оптимизатор пытается найти лучший тип данных для использования. А также часто генерирует некоторую дополнительную метаинформацию, такую ​​как hidden clases или tags для массивов.

Есть несколько очень хороших статей о типах данных:

Итак, почему массивы, созданные с помощью JSON.parse, медленны? Парсер при создании значений неправильно оптимизирует структуры данных, и в результате мы получаем массивы untagged с boxed удваиваем. Но мы можем оптимизировать массивы с помощью Array.from, а в вашем случае, так же как сгенерированные массивы, вы получаете массивы smi с номерами smi. Вот пример, основанный на вашем примере.

const fs = require('fs');
const path = require('path');
const outputFilePath = path.join(__dirname, process.argv[2] || 'test-data.json');

let tests = JSON.parse(fs.readFileSync(outputFilePath));

// for this demo we take only the first items array
var arrSlow = tests[0].input;
// `slice` copies array as-is
var arrSlow2 = tests[0].input.slice();
// array is copied and optimized
var arrFast = Array.from(tests[0].input);

console.log(%HasFastSmiElements(arrFast), %HasFastSmiElements(arrSlow), %HasFastSmiElements(arrSlow2));
//> true, false, false
console.log(%HasFastObjectElements(arrFast), %HasFastObjectElements(arrSlow), %HasFastObjectElements(arrSlow2));
//> false, true, true
console.log(%HasFastDoubleElements(arrFast), %HasFastDoubleElements(arrSlow), %HasFastDoubleElements(arrSlow2));
//> false, false, false

// small numbers and unboxed doubles in action
console.log(%HasFastDoubleElements([Math.pow(2, 31)]));
console.log(%HasFastSmiElements([Math.pow(2, 30)]));

Запустите его с помощью node --allow-natives-syntax test.js

Ответ 2

ОК... прежде всего давайте поговорим о стратегии тестирования...

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

https://docs.google.com/spreadsheets/d/1Z95GtT85BljpNda4l-usPjNTA5lJtUmmcY7BVB8fFGQ/edit?usp=sharing

После обновления теста (выполняется 100 тестов в строке и вычисления среднего значения), я считаю, что основное различие во времени выполнения:

  • indexOf и для циклов работают лучше в сценарии GENERATOR
  • поиск в бинарном поиске и интерполяции лучше работает в сценарии JSON-parse

Пожалуйста, просмотрите документ google раньше...

ОК.. Великий... Это гораздо проще объяснить... в основном мы оказались в ситуации, когда RANDOM доступ к памяти (двоичный, интерполяционный поиск) и Доступ к памяти CONSECUTIVE (indexOf, for) дают разные результаты


Хммм. Давайте углубимся в модель управления памятью NodeJS

Прежде всего, NodeJS имеет несколько представлений массивов, я действительно знаю только два - numberArray, objectArray (означает массив, который может включать значение любого типа)

Давайте посмотрим на сценарий GENERATOR:

Во время создания начального массива NodeJS ABLE, чтобы обнаружить, что ваш массив содержит только числа, поскольку массив, начинающийся с только чисел, и ничего не добавляется к нему. Это приводит к использованию простой стратегии распределения памяти, только сырой ряд целых чисел, идущих один за другим в память...

Массив представлен как array of raw numbers в памяти, скорее всего, таблица пейджинга памяти имеет здесь эффект

Этот факт ясно объясняет, почему Доступ к памяти CONSECUTIVE работает лучше в этом случае.

Давайте посмотрим на сценарий JSON-parse:

Во время JSON синтаксическая структура JSON непредсказуема (NodeJS использует анализатор потока JSON (доверие 99,99%)), каждое значение трактуется как наиболее удобное для разбора JSON, поэтому...

Массив представлен как array of references to the numbers в памяти, просто потому, что при разборе JSON это решение более эффективно в большинстве случаев (и никто не заботится (devil))

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

Также в этой модели доступ к RANDOM-памяти дает лучшие результаты, потому что у NodeJS-движка нет опций - для оптимизации времени доступа он создает хорошие префиксное дерево или хэш-карта, которая дает постоянное время доступа в сценариях СЛУЧАЙНАЯ память p >

И это довольно хорошее объяснение, почему побеждает сценарий JSON-parse во двоичном, интерполяционном поиске