2017-09-18 44 views
12

我无法解释为什么我的性能测试在2种不同类型的运行中返回显着不同的结果。process.hrtime()的执行时间返回完全不同的结果

步骤来重现问题:

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

practice1.generator应该产生一个test-data.json文件,并登录一些搜索算法执行时间放入控制台。 之后,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相对于其他算法的情况下,在执行时间上的差异。

如果我反复运行node practice1.generatornode 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.7ms在practice1.generator(使用JSON.parse -d对象即使)和〜2.3ms在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);

+0

您运行的是什么版本的节点相当不错的解释,为什么在二进制JSON-解析方案获胜,插值搜索? –

+0

嗨@SamH。我正在使用node v6.11 – AVAVT

+0

刚刚在8.5.0 Current上测试过并得到了相同的结果。 – AVAVT

回答

2

正如你已经注意到了,在性能上的差异导致了比较:generated array VS JSON.parse d。在这两种情况下我们有什么:具有相同数字的相同数组?所以查找性能必须相同?号

每个Javascript引擎具有用于表示相同的值的各种数据类型的结构(数字,对象,数组等等)。在大多数情况下,优化器会尝试找出要使用的最佳数据类型。并且还经常生成一些额外的元信息,例如数组为hidden clasestags

有关于数据类型几个非常漂亮的文章:

那么,为什么由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

1

OK ......首先,让我们来谈谈测试策略来看,它...

运行这个测试几次给出令人难以置信的不同的结果波动很多关于每个点。 ..在这里看到的结果

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

测试更新后(运行100次测试连续和CAL culating平均)我得分,在执行时间的主要区别是:

  • 的的indexOf和循环在发电机的情况更好的工作
  • 二进制搜索和插值搜索正在以JSON-解析方案
  • 更好

请看谷歌文档之前...

OK ..大...这东西是更容易解释...basicly我们陷入的情况时随机内存访问(二进制,插搜索)和连续的内存访问(的indexOf,对于)给出不同的结果


嗯。让我们去更深的NodeJS

内存管理模式

首先的NodeJS有几个阵列表示,其实,我知道里面只有两个 - numberArrayobjectArray(指阵列可以包括任何类型的值)

让看看发电机的情景:

在最初创建数组是的NodeJS ABLE检测到您的阵列只包含数字,如数组开始只有数字,没有其他类型被添加到它。这导致使用简单的内存分配策略,只是原始行整数,将一个一个在内存中...

阵列被表示为内存array of raw numbers,很可能只memory paging table在这里有

的效果显然这个事实解释为什么连续内存访问在这种情况下效果更好。

让我们看一下JSON-解析情景:

在JSON的JSON解析结构是不可预测的(的NodeJS使用流JSON解析器(99.99%的置信度)),每个值牙牙作为最舒适对于JSON解析,所以......

阵列被表示为内存array of references to the numbers,只是因为在解析JSON这个解决方案是提高生产力在大多数情况下(无人问津(魔鬼))

据我们异体在堆cating内存小块内存被填充更流畅的方式在这个模型随机内存访问提供了更好的结果

而且,因为的NodeJS引擎没有选项 - 优化它创造了良好的访问时间要么prefix tree要么hash map这给定访问时间随机内存访问场景

这是

+0

非常感谢您的回答。我从这两个答案中学到了很多东西,但是由于我的奖金已经接近尾声,我不得不选择十位数,因为它提供了代码和文章形式的证明,专门用于V8。 – AVAVT

+1

好的......只是......他回答了另一个问题给你......对我来说,最难回答的问题是解释为什么你只在binaty搜索中遇到“Perfomance gain”......无论如何感谢你的问题,我有很大的时间考虑它! –

相关问题