limlog は、マルチスレッド環境でも 1 マイクロ秒台のレイテンシを維持しながら、確実にログをファイルへ書き出す軽量ロギングライブラリです。本稿では内部構造、性能チューニング、および使用方法を詳述します。
設計目標
- 確実性:すべてのログが欠損なく出力され、スレッド間でログが混在しない。
- 可読性:1 行 1 ログ、awk 等での解析を容易にする。
- 利便性:printf よりも cout に近い API、自動ローテーション、ソースコード 2000 行以内。
- 性能:CPU 数以内でスレッドを増やしても 30 % 以内の劣化、1 ログ 1 µs を目標。
ログフォーマット
ファイル名 : app.YYYYMMDD.HHMMSS.連番.log 一行フォーマット : YYYYMMDD HH:MM:SS.usec TID LEVEL メッセージ - ファイル:関数名:行
公開インターフェース
マクロを用いた簡潔な API を提供します。
#include <limlog/limlog.hpp>
int main() {
limlog::set_file("app");
limlog::set_level(limlog::Level::DEBUG);
limlog::set_roll_size(64); // MB
LOG_INFO << "start";
LOG_WARN << "value=" << 42;
LOG_ERROR << "oops" << limlog::end;
}
アーキテクチャ
フロントエンド(ログ生成)とバックエンド(ログ出力)を完全に分離し、thread_local バッファとシングルコンシューマスレッドを採用してロックフリーに近い動作を実現します。
バックエンド
唯一の LimLog シングルトンが、起動時に専用スレッドを生成し、各スレッドの thread_local BlockingBuffer を巡回してファイルへ書き込みます。
class LimLog {
...
std::vector<BlockingBuffer*> buffers_;
std::thread sink_;
std::mutex mtx_;
std::condition_variable cv_;
char* out_buf_ = nullptr; // 16 MB
static thread_local BlockingBuffer* local_buf_;
};
シャットダウン時は二段階の条件変数で残留データを確実にフラッシュします。
フロントエンド
LogLine 一時オブジェクトが operator<< で内部バッファに直接書き込みます。
struct LogLine {
LogLine(Level lv, const char* file, const char* func, int line);
template<typename T>
LogLine& operator<<(const T& v) {
// 高速フォーマット後、バックエンドへ転送
return *this;
}
};
性能最適化
時刻取得
- Linux では
clock_gettime(CLOCK_REALTIME_COARSE)を使用し、秒単位でキャッシュ。 - マイクロ秒以下は
% 1'000'000で算出。
スレッド ID
static thread_local pid_t cached_tid = 0;
pid_t get_tid() {
if (cached_tid == 0) cached_tid = syscall(SYS_gettid);
return cached_tid;
}
整数→文字列変換
桁ごとの事前計算テーブルを用いた逐次除算方式により、std::to_string より 3~4 倍高速化。
ベンチマーク
Intel Core i7-9700K、WSL1、ext4 環境で測定。
| スレッド数 | 平均レイテンシ (µs/1 ログ) | スループット (MB/s) |
|---|---|---|
| 1 | 0.55 | 180 |
| 4 | 0.61 | 175 |
| 8 | 0.72 | 170 |
1 ログ 80 byte 想定。8 スレッドでも 30 % 以内の劣化に収まっています。
テストスイート
test_timestamp():日付ロールオーバー検証test_blocking_buffer():リングバッファ境界条件test_itoa():整数変換精度チェック
今後の課題
- 浮動小数点数の高速フォーマット(Ryu アルゴリズム導入)
- 時刻取得のコスト削減(TSC ベアリング)
- 非同期ローテーション(ディスク I/O ブロック回避)