危機產生
在一次對 TFL 進行 QC 的過程中,偶然發現某個宏程式在執行過程中會消耗大量記憶體,最高記憶體使用量甚至達到了 12GB。
分析原因
該宏程式(%ReadRTF)的作用是讀取一個 RTF 檔案,嘗試解析 RTF 檔案中的表格儲存的資料,並將這些資料轉換為 SAS 資料集。將 RTF 檔案轉換為 SAS 資料集的作用是便於 QC 人員直接在資料集上使用 PROC COMPARE
過程進行對比,準確高效,而不必用肉眼將 SAS 結果視窗的內容與 RTF 文件進行比對。對於一些篇幅較長的清單,使用肉眼進行 QC 工作幾乎是不可能的。
並非所有 RTF 檔案都會導致在執行 %ReadRTF 時消耗大量記憶體,大部分情況下,由於原始 RTF 檔案的體積通常很小(10~200KB),%ReadRTF 執行過程中的記憶體使用量很難被輕易察覺。
暴露記憶體使用問題的是一張 6.9MB 的清單,它包含近 3900 條觀測,以及 17 個變數,合計約 66300 個資料點,體現在 RTF 程式碼上,近 14 萬行。
FULLSTIMER 系統選項
為了進一步找到佔用大量記憶體的罪魁禍首,在執行 %ReadRTF 前指定 FULLSTIMER
系統選項,以便在日誌中顯示完整的系統資源資訊,具體如下:
- 實際時間:執行 SAS 任務所消耗的總時間
- 使用者 CPU 時間:執行使用者程式碼所消耗的時間
- 系統 CPU 時間:執行系統呼叫所消耗的時間
- 記憶體:執行某個步驟所需的記憶體量
- OS 記憶體:執行某個步驟向系統申請的最大記憶體量
指定 FULLSTIMER
後,嘗試執行 %ReadRTF,在給出的日誌中,定位到了問題所在,如下圖:
這是一個用於對資料集排序的 PROC SORT
過程,在這次呼叫中,它向系統申請(OS 記憶體)並消耗(記憶體)了約 12GB 的記憶體。
檢視原始碼:
proc sort data = _tmp_rtf_context(where = (flag_data = "Y")) out = _tmp_rtf_context_sorted(compress = &compress);
by obs_seq obs_var_pointer;
run;
proc transpose data = _tmp_rtf_context_sorted out = _tmp_outdata prefix = COL;
var context;
id obs_var_pointer;
by obs_seq;
run;
結合原始碼上下文,可以得知這裡使用 PROC SORT
過程的目的是為了確保讀取 RTF 檔案後,中間處理步驟中的資料集按照正確的順序對觀測進行排序,以便下一步使用 PROC TRANSPOSE
進行轉置時,by
語句不會產生錯誤。
進一步研究 RTF 語法,發現在 RTF 1.6 Specification 中,表格的每一行被視為一個段落,表格行與行之間的相對位置關係與對應 RTF 程式碼定義的相對位置關係一致,也就是說,使用 SAS 軟體生成 TFL 後,再呼叫 %ReadRTF 進行讀取,觀測的排序極有可能已經完成了。
對於資料量比較小的 RTF 檔案,重複排序帶來的記憶體消耗微乎其微,但若遇到資料量龐大的 RTF 檔案,重複排序導致的記憶體消耗問題就不容忽視了。
危機解決
查閱幫助文件,發現 PROC SORT
過程提供了一個選項 PRESORTED
,這個選項可以在強烈懷疑資料集已經正確排序的情況下使用。
若指定了 PRESORTED
選項,SAS 系統會在排序之前,檢查輸入資料集內觀察結果的順序是否正確。若正如所預料的那樣已經完成了排序,則不會重複排序,由於“檢查是否排序”比“直接進行排序操作”本身更快且消耗的記憶體更少,故在上述場景下,PRESORTED
選項的確帶來了效能的可觀提升。
如下是使用 PRESORTED
選項後,%ReadRTF 執行過程中,排序操作消耗的記憶體資源:
可以發現,在這次呼叫中,PROC SORT
過程向系統申請和消耗的記憶體量降低了 3 個數量級。此外,由於檢查排序的計算量小得多,過程步所消耗的時間也下降不少。
總結
FULLSTIMER
選項可顯示程式步(DATA 步和 PROC 步)消耗的完整系統資源,便於開發者對程式進行最佳化PRESORTED
選項可在特定情況下避免重複排序,降低系統資源消耗
注意:PRESORTED
選項應當僅在非常確定資料集可能已經處於正確排序的狀態下使用,若 SAS 檢查發現輸入資料集未遵循排序順序,則依然會進行一次排序操作。