2017-09-18 45 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」......無論如何感謝你的問題,我有很大的時間考慮它! –

相關問題