簡體   English   中英

確定性的執行時間度量

[英]A deterministic execution time measure

一些算法依賴於時間度量。 例如,10% 的時間,遵循方法 A。如果這不起作用,則在 20% 的時間遵循 B。 如果這不起作用,請執行 C。

以秒為單位測量執行時間是不確定的。 緩存狀態、在內核上交錯非用戶任務,甚至只是現代處理器時鍾速度的動態提升都是改變其他確定性代碼執行時間的外部影響。 因此,如果使用經典的執行時間度量,該算法可能會表現出非確定性。

為了保持算法的行為確定性,我正在尋找一種確定性的方法來測量執行時間。 這是可能的,例如,CPLEX 求解器具有稱為ticks的確定性時間度量。

我知道這個簡單的問題沒有簡單的答案。 所以讓我把它縮小一點:

  • 確定性屬性是一個硬約束。 我寧願有一個與測量的執行時間只有非常微弱相關的度量,只要它是確定性的。
  • 理想情況下,確定性時間度量測量整個程序執行,包括靜態編譯的庫。 但如果這是不可能的,那么測量我可以修改的源代碼的執行時間就可以了。
  • 我願意承受 100% 的性能損失,但不會更多。 不過,性能影響越小越好:)
  • 如果編譯后的二進制文件在不同 CPU 型號之間不再可移植,那也沒關系。

我考慮過一些方法,但不知道它們實施起來有多難或它們的效果如何:

  • 修改編譯器以在編譯代碼中的每個其他命令之間添加一個增加全局計數器的命令。 這似乎是最有原則的方法,理論上甚至可能適用於靜態編譯的庫。
  • 計算內存訪問次數。 不知道如何在實踐中做到這一點。 可能也通過修改編譯器?
  • 使用源代碼中的全局計數器計算 if 語句和循環條件檢查的數量。 這可以通過例如宏輕松完成,但它會忽略許多庫調用(例如,對向量進行排序的簡單調用不會增加計數器),因此可能與實際執行時間沒有太大關聯。
  • 訪問硬件性能計數器,例如,計算進程的指令數,可能是通過諸如PAPI 之類的庫。 這里的問題是我認為這些計數器也是不確定的?

那么,如何確定性地衡量程序的執行時間呢?

編輯:測量 CPU 時間(例如通過clock()函數)絕對比我天真的掛鍾時間示例更好。 但是,測量 CPU 時間絕不是確定性的:運行相同的確定性程序將產生不同的 CPU 時間。 我真的在尋找一個確定性的度量(或@mevets 所說的“完成的工作”的度量)。

您可以通過調用 C 標准庫函數clock()訪問進程時間(進程使用的時鍾周期數)而不是掛鍾時間(經過的時間,包括在其間進行上下文切換的任何其他進程clock() 一秒鍾內有CLOCKS_PER_SEC時鍾滴答。 請注意,如果您的程序是多線程的,這可能比掛鍾時間運行得更快——即,它測量程序在所有處理器內核上消耗的時鍾周期。 因此, CLOCKS_PER_SEC時鍾滴答是指一個處理器內核上的一秒計算時間。 要實現方法之間的切換,您可以使用異步 I/O(例如使用新奇的 C++20 協程或 Boost 協程),偶爾檢查進程時間,或者您可以執行定時軟件中斷,設置一個標志由主執行線程執行,然后切換到新方法。

您可能不想在每條指令后增加計數器。 這會產生巨大的計算開銷並占用您的處理器管道,因為所有其他指令都依賴於它之前的指令 2,以及您的指令緩存。

代碼示例(POSIX):

static /* possibly thread_local */ std::atomic<int> method;
void interrupt_handler(int signal_code) {
    method.fetch_add(1);
}

void calculation(/* input */) {
    auto prev_signal_handler = signal(SIGINT, &interrupt_handler);
    
    try {
        method.store(0);
        int prev_method = 0;

        // schedule timer interrupts
        for (size_t num_ns : /* list of times, in ns */) {
            timer_t t_id;
            sigevent ev;
            ev.sigev_notify = SIGNAL;
            ev.sigev_signo = SIGINT;
            ev.sigev_value.sival_ptr = &t_id;
            timer_create(CLOCK_THREAD_CPUTIME_ID, &ev, &t_id);
            itimerspec t_spec;
            t_spec.it_interval.tv_sec = t_spec.it_value.tv_sec = num_ns / 1000000000;
            t_spec.it_interval.tv_nsec = t_spec.it_value.tv_nsec = num_ns % 1000000000;
            timer_settime(t_id, 0, &t_spec, nullptr);
        }

        bool done = false;
        while (!done) {
            int current_method = method.load();
            if (current_method != prev_method) {
                // switch method
            }
            else {
                // continue using current method
            }
        }
    }
    catch (...) {
        signal(SIGINT, prev_signal_handler);
        throw;
    }
    
    signal(SIGINT, prev_signal_handler);
}

您陷入了一些可能會廣泛更改代碼的詳細解決方案中,可能是因為這些是您熟悉的唯一方法,但恕我直言,這是短視的。 此時您無法確定以這種侵入性方式檢測生成的代碼是否有價值。 讓我們退后一步。

一些算法依賴於時間度量。 例如,10% 的時間,遵循方法 A。如果這不起作用,則在 20% 的時間遵循 B。 如果這不起作用,請執行 C。

我不認為這是真的。 這是一個任意約束,根本不通用。 算法依賴於“努力”,而且通常實時是努力的一個非常糟糕的替代品。 正如您所說的那樣,任何類型的“時間”都深陷於架構細節中。

另一個問題是假設算法是變化的單位。 它們通常不是,即您在這里沒有您想象的那么多控制權,除非您在匯編中對所有數字部件進行編碼,或者徹底審核生成的代碼。 由於生成的代碼在運行時所做的與體系結構相關的選擇,每個算法如果成功,可能會產生略有不同的結果,具體取決於數值錯誤堆棧。 這是一回事,編譯器和/或它們的運行時庫做了很多! 因此,只要您的目標是顯示它不正確,在各種 PC 上運行相同的編譯浮點代碼並產生位相同結果的想法是正確的,但實際上它會在稍后的某個時間證明是不正確的太深入它而無法實際實施修復所需的巨大更改。

但是在算法內部,您應該有很多可以增加計數器的任意點 - 不要太頻繁,並使用計數器的值作為算法所花費的努力的衡量標准。 對於每種算法,這種度量具有與“實時”不同的比例因子並不重要,因為這里的真正要求不是實時。 您真正想要的是某種確定性的方式來執行切換算法的決定,並且您可以將這些任意切換點粗略地校准為實時一次,並保持這種校准凍結:這並不重要,只要您可以清楚地決定何時切換。

此外,當算法產生非常接近努力閾值的結果(“收斂”)時,需要注意一些問題。 由於架構差異,在固定浮點閾值方面實現“收斂”所需的確切工作可能在 CPU 代之間略有不同。 因此,不是硬截止,您需要某種表達滯后的方式,以便如果收斂發生在努力截止附近,則使用更多替代標准用於閾值或收斂,但您需要做適當的統計建模以表明替代方案足夠可靠。

計數器可以處理工作單位,但每個單位的價值(即時間)是否相等? 服務時鍾(3) 提供了一個近似的虛擬執行時間——即您的進程實際運行時經過的時間,而不是現實世界(牆)時間。

類似地,timer_create 可以接受類似於 CLOCK_PROCESS_CPUTIME_ID 的時鍾 ID,它允許您在某個 CPU 時間過去后發出信號。 如果您的應用程序可以在不進入未定義狀態的情況下被任意中斷,您可以使用它從方法 1 -> 2 -> 3 切換。

盡管比計算工作塊要好,但您需要接受准確時間周圍的某些不准確性,以考慮系統開銷、緩存爭用等。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM