process.hrtime()的执行时间返回的结果大不相同

我无法解释为什么我的性能testing在两种不同types的运行中返回显着不同的结果。

重现问题的步骤:

  1. 从gist获取代码: https : //gist.github.com/AVAVT/83685bfe5280efc7278465f90657b9ea
  2. 运行node practice1.generator
  3. 运行node practice1.performance-test

practice1.generator应该生成一个test-data.json文件,并将一些searchalgorithm执行时间logging到控制台中。 之后, practice1.performance-testtest-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 

注意indexOfbinary search相比其他algorithm执行时间的差异。

如果我反复运行node practice1.generator 或者 node practice1.performance-test ,结果是相当一致的。

现在这是非常麻烦的,我找不到一个办法来找出哪个结果是可信的,以及为什么会出现这种差异。 是由生成的testing数组与JSON.parse-dtesting数组之间的差异引起的? 或者是由process.hrtime()引起的? 或者是我甚至无法理解的一些未知的原因?


更新 :我已经追溯了indexOf情况的原因是因为JSON.parse 。 在practice1.generatortests数组是原始的生成数组; 而在practice1.performance-test数组是从json文件中读取,可能与原始数组有所不同。

如果在practice1.generator内,而不是JSON.parse()从string中的新数组:

 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 

所以至less我知道indexOf在原始数组上运行得更好,在JSON.parse -d数组上更糟糕。 不过我只知道原因,不知道为什么。

二进制search执行时间在2个文件上保持不同 ,在practice1.generator (即使使用JSON.parse -d对象)中持续约1.7ms,在practice1.performance-test持续约2.3ms。


以下是与要点相同的代码,供将来参考之用。

 /* * 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); 

正如你已经注意到的那样,性能差异导致了比较: generated array vs vs JSON.parse d。 我们在两种情况下都有:相同数组的相同数组? 所以查阅performance必须一致? 没有。

每个Javascript引擎都有各种数据types结构来表示相同的值( 数字,对象,数组等 )。 在大多数情况下,优化器会尝试找出使用的最佳数据types。 而且通常还会生成一些额外的元信息,如hidden clases的数组或tags

关于数据types有几个非常好的文章:

  • V8-PERF /数据types
  • V8性能提示

那么为什么由JSON.parse创build的数组很慢呢? parsing器在创build值时不会正确地优化数据结构,因此我们得到带有boxed双精度的untagged数组。 但是,我们可以使用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运行它

好的…首先让我们谈谈testing策略…

多次运行这个testing,给出了令人难以置信的不同结果,每个点都有很大的波动…在这里查看结果

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

testing更新(连续运行100次testing并计算平均值)后,我得出执行时间的主要区别是:

  • 在GENERATOR场景中indexOf和for循环工作得更好
  • 二进制search和插值search在JSONparsing场景中工作得更好

请看一下google文档之前…

OK ..好…这个东西更容易解释…基本上我们陷入了当RANDOM内存访问 (二进制,插值search)和CONSECUTIVE内存访问 (indexOf,for)给出不同结果的情况


嗯。 让我们深入NodeJS的内存pipe理模型

首先NodeJS有几个数组表示,我其实只知道两个 – numberArrayobjectArray (意味着数组可以包含任何types的值)

让我们看看GENERATOR场景:

在初始数组创build期间, NodeJS能够检测到您的数组只包含数字,只能从数字开始数组,而没有其他types被添加到数组中。 这导致使用简单的内存分配策略,只是在内存中一个接一个的整数行。

数组被表示为内存array of raw numbers ,很可能只有内存分页表在这里有效

这个事实清楚地解释了为什么CONSECUTIVE内存访问在这种情况下更好地工作。

让我们看看JSONparsing场景:

在JSON的JSONparsing结构是不可预知的(NodeJS使用streamJSONparsing器(99.99%的置信度)),每个值被视为最舒适的JSONparsing,所以…

数组被表示为array of references to the numbers内存中array of references to the numbers ,仅仅因为在parsingJSON的时候,这个解决scheme在大多数情况下更有效率(而且没有人关心(恶魔))

就我们用小块来分配内存而言,内存的填充更加stream畅

同样在这个模型中, RANDOM内存访问提供了更好的结果,因为NodeJS引擎没有选项 – 为了优化访问时间,它创build了好的前缀树,或者在RANDOM内存访问场景中给出了不变的访问时间

这是解释为什么JSONparsingscheme在二进制,插值search中获胜的原因