簡體   English   中英

減少條件跟蹤/記錄調用的開銷

[英]Overhead reduction of conditional trace/logging calls

為了跟蹤和調試Java代碼,我使用了一個簡單的Util類,而不是一個成熟的日志框架:

public class Util {
    public static int debugLevel;

    public static void info(String msg) {
      //  code for logfile output
      //  handling of unprintable characters
      //  etc. omitted 
      System.out.println(msg);
    }

    public static void info4(String msg) {
        if (debugLevel >= 4) {
           info(msg);
        }
    }
}

這允許緊湊的單行語句,如下所示:

info4(String.format("%16s: ", host) + Util.toHex(sidNetto));

使用debugLevel變量,我可以控制程序的詳細程度。 通常,調試級別是在執行開始時全局設置的。 但是它也可以在常規級別上進行本地調整。

基本上,我將重復的if (debugLevel >= DEBUG_ALL) {...}括號保存在跟蹤調用周圍。 但是,無論調試級別如何,都必須在運行時准備並傳遞調用的參數。

我的問題:

我怎樣才能輕推編譯時優化器或JVM來刪除多余的跟蹤調用? 我在考慮C/C++函數內聯。

這里討論了有關C#一個相關問題。 但是我不確定如何將建議的答案移植到Java 2010年的另一篇相關文章討論了類似的方法。 我想知道是否真的需要像ProGuard這樣的第三方工具來解決這樣一個常見的任務。

大多數日志記錄框架就是這樣做的。 對於輕量級參數(最好是包括內置格式化程序),請不要檢查級別,否則請在序列化復雜的字符串參數之前檢查級別。

您可以使用Java 8 java.util.functions.Supplier<String>進行花邊評估,但是我認為在顯式級別的測試用例上可能無法獲得任何性能。

記錄器如下所示:

void debug(String ptrn, Supplier<String> args...)

您可以像這樣使用它:

debug("Hello {0}", this::getName());

我知道的大多數日志記錄API建議在實際調用log方法之前先檢查是否啟用了日志級別,以防必須先准備消息,例如:

if (logger.isTraceEnabled()) {
    String msg = String.format("Name changed from %s to %s", oldName, newName);
    logger.trace(msg);
}

一些日志記錄API(例如SLF4J)還提供了更復雜的日志記錄方法,這些方法接受格式字符串和多個參數,因此僅在啟用日志級別的情況下才生成日志消息:

logger.trace("Name changed from {} to {}", oldName, newName);

在大多數情況下,這已經足夠了,但是有時您的消息構建起來會更復雜,或者必須先將參數轉換為字符串。 在這種情況下,檢查日志級別仍然是一種好方法。

從Java 8開始,您還可以利用Lambda表達式解決此問題。 您的日志方法可以這樣實現:

public void log(Supplier<String> messageSupplier) {
    if (isLogEnabled()) {
        String msg = messageSupplier.get();
        // TODO: log msg
    }
}

如您所見,僅在啟用日志記錄的情況下,才從messageSupplier檢索消息。 多虧了lambda表達式,實現Supplier<String>非常容易:

logger.log(() -> String.format("Name changed from %s to %s", oldName, newName));

更新(感謝Joshua Taylor)

從Java 8開始,java.util.logging API已經支持消息提供者,例如,請參閱Logger#info ,因此您可以通過JRE的“內置”解決方案輕松地交換日志記錄實現。

由於它們的復雜性,似乎不使用已建立的日志記錄框架很奇怪,但是擔心諸如方法內聯之類的較小優化,卻忽略了格式化日志字符串的更大問題,而與日志級別無關。 但是,如果您堅持重新發明輪子:

JVM(至少是Oracle熱點JVM)自動內聯短方法,並對無法訪問的分支執行無效代碼消除。 要檢測為無法到達,消息的日志級別和級別閾值必須是恆定的(編譯時常數或靜態最終值)。 否則,JVM仍將比較每次調用的日志記錄級別,盡管它仍可能執行推測性內聯(內聯通常采用的分支,由條件分支指令保護),以確保僅在異常情況下執行分支指令。

但是,更令人擔憂的是構建日志消息的成本,只有在必須實際記錄該消息的情況下才產生該消息。 在准備消息之前,要求調用代碼檢查是否已啟用日志記錄的舊log4j方法相當冗長,很容易被遺忘。 相反,SLF4J通過使log方法采用格式字符串和可變數量的要插入占位符的對象,將字符串連接推遲到日志記錄系統。 SLF4J常見問題解答寫道

以下兩行將產生完全相同的輸出。 但是,在禁用日志記錄語句的情況下,第二種形式的性能將比第一種形式高至少30倍。

 logger.debug("The new entry is "+entry+"."); logger.debug("The new entry is {}.", entry); 

值得注意的是,參數(此處為entry )的類型為Object ,因此僅當實際上必須記錄消息時才將其轉換為String

需要明確的是,沒有可靠的方法可以通過重新定義方法來跳過對方法參數的求值,因為只有在即時編譯器可以證明該求值沒有副作用的情況下,這種消除才可能發生,熱點jvm僅在其發現時才進行檢測已內聯整個評估,僅用於非常簡單的評估。 因此,將格式轉移到日志記錄系統中的API解決方案可能是您所希望的最好的解決方案。

暫無
暫無

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

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