參考/部分翻譯:https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array

Why is processing a sorted array faster than processing an unsorted array?

前言

在撰寫程式時,評估時間複雜度是一個非常重要的步驟,以確保程式運行的足夠快,滿足需求。

舉例而言,排序陣列是一個耗時的操作,減少使用通常能大幅降低程式的執行時間。但是,在 Stack Overflow 上,卻出現了一個奇妙的問題:「為什麼程式碼中多寫了一行排序,會使得整個程式變快呢?」

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // 建立大小 32768 的亂數陣列
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // 多加了一行排序,下方程式會變快約 3 倍
		// aka 移除這行,下方程式會大幅度變慢
    std::sort(data, data + arraySize);

    clock_t start = clock();
    long long sum = 0;

		// 為凸顯效果,重複主迴圈 100000 次
    for (unsigned i = 0; i < 100000; ++i)
    {
				// 主迴圈:遍歷亂數陣列,計算大於 128 的值的和
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

問題分析

上述的程式目的是:找出陣列內大於 128 的數字,並將其加總。 整個程式分成三部分:

  1. 先建立一個陣列,並填入亂數
  2. 利用 std::sort 來排序這個陣列
  3. 利用 if 判斷是否符合條件,若是則加總

其中,第二步驟的「排序」與程式邏輯並無關聯,我們嘗試看看移除與否對性能造成的影響。

事先排序的影響

顯而易見的,「有事先排序」的版本比「無事先排序」的版本快。這主要是受到了 分支預測(branch prediction)的影響。

背景知識

想像一個情況,在通訊不發達的 1800 年代,你站在改變列車軌道的操縱杆旁,你並不知道下一輛列車想往左還往右,因此你只能每次都請列車長減速,並告訴你他的目的地,然後你再以此判斷。

顯然這是一個很沒有效率的做法,因為不論列車目的為何,都需要減速與你溝通。

有一個更好的想法是,我們來猜下一輛列車的目的地!

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7b071186-5df2-481d-8f67-b696b2f7358e/Untitled.png

所以如果你總是都能猜對,列車將不需要停下,節省了大量時間。如果你時常猜錯,那麼就會耽擱許久。而這就是分支預測的基本概念!

現代 CPU 就和你一樣,手握著操縱杆。當程式有 if 語句時,就會被迫面臨二選一的運氣大挑戰。如下圖,cmp 指令代表先比較誰大誰小,jle 代表左邊 ≤ 127 則跳過 25 行。

Untitled

這大體上就是分支預測的概念,雖然火車並不是很好的類比,實際上 CPU 是因為 pipeline 的緣故才需要去猜測分支方向。