更多文章
概念
一個編寫良好的計算機程式常常具有良好的區域性性,它們傾向於引用鄰近於其他最近引用過的資料項的資料項,或者最近引用過的資料項本身,這種傾向性,被稱為區域性性原理。有良好區域性性的程式比區域性性差的程式執行得更快。
區域性性通常有兩種不同的形式:
- 時間區域性性
在一個具有良好時間區域性性的程式中,被引用過一次的記憶體位置很可能在不遠的將來被多次引用。
- 空間區域性性
在一個具有良好空間區域性性的程式中,如果一個記憶體位置被引用了一次,那麼程式很可能在不遠的將來引用附近的一個記憶體位置。
時間區域性性示例
function sum(arry) {
let i, sum = 0
let len = arry.length
for (i = 0; i < len; i++) {
sum += arry[i]
}
return sum
}
在這個例子中,變數sum在每次迴圈迭代中被引用一次,因此,對於sum來說,具有良好的時間區域性性
空間區域性性示例
具有良好空間區域性性的程式
// 二維陣列
function sum1(arry, rows, cols) {
let i, j, sum = 0
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
sum += arry[i][j]
}
}
return sum
}
空間區域性性差的程式
// 二維陣列
function sum2(arry, rows, cols) {
let i, j, sum = 0
for (j = 0; j < cols; j++) {
for (i = 0; i < rows; i++) {
sum += arry[i][j]
}
}
return sum
}
再回頭看一下時間區域性性的示例,像示例中按順序訪問一個陣列每個元素的函式,具有步長為1的引用模式。
如果在陣列中,每隔k個元素進行訪問,就稱為步長為k的引用模式。
一般而言,隨著步長的增加,空間區域性性下降。
這兩個例子有什麼區別?區別在於第一個示例是按照列順序來掃描陣列,第二個示例是按照行順序來掃描陣列。
陣列在記憶體中是按照行順序來存放的,結果就是按行順序來掃描陣列的示例得到了步長為rows的引用模式;
而對於按列順序來掃描陣列的示例來說,其結果是得到一個很好的步長為1的引用模式,具有良好的空間區域性性。
效能測試
執行環境
- cpu: i5-7400
- 瀏覽器: chrome 70.0.3538.110
對一個長度為9000的二維陣列(子陣列長度也為9000)進行10次空間區域性性測試,時間(毫秒)取平均值,結果如下:
所用示例為上述兩個空間區域性性示例
按列排序 | 按行排序 |
---|---|
124 | 2316 |
從以上測試結果來看,二維陣列按列順序訪問比按行順序訪問快了1個數量級的速度。
總結
- 重複引用相同變數的程式具有良好的時間區域性性
- 對於具有步長為k的引用模式的程式,步長越小,在記憶體中以大步長跳來跳去的程式空間區域性性會很差
測試程式碼
const arry = []
let [num, n, cols, rows] = [9000, 9000, 9000, 9000]
let temp = []
while (num) {
while (n) {
temp.push(n)
n--
}
arry.push(temp)
n = 9000
temp = []
num--
}
let last, now, val
last = new Date()
val = sum1(arry, rows, cols)
now = new Date()
console.log(now - last)
console.log(val)
last = new Date()
val = sum2(arry, rows, cols)
now = new Date()
console.log(now - last)
console.log(val)
function sum1(arry, rows, cols) {
let i, j, sum = 0
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
sum += arry[i][j]
}
}
return sum
}
function sum2(arry, rows, cols) {
let i, j, sum = 0
for (j = 0; j < cols; j++) {
for (i = 0; i < rows; i++) {
sum += arry[i][j]
}
}
return sum
}