【テクニカル・上級編】Clangの『コードカバー率』解析を極める:ソースコードの死に体を探すllvm-cov活用ガイド – 実行環境・ランタイム・コンパイラ生産性向上バイブル

Clangの『コードカバー率』解析を極める:ソースコードの死に体を探すllvm-cov活用ガイド

大規模なC/C++コードベースにおいて、技術負債の最大の温床となるのは「誰も存在理由を説明できないが、怖くて消せないレガシーコード」である。長年運用されてきたシステムにおいて、デッドコードは単なるストレージの無駄遣いではない。セキュリティ脆弱性の隠れ家となり、リファクタリングの認知負荷を無限に引き上げる癌細胞だ。

世の中の多くのチュートリアルは、「カバレッジを測定して100%を目指そう」という精神論で終わっている。しかし、真にプロダクトの寿命を延ばすべきDevOpsアーキテクトにとって、カバレッジ測定は「テストが通っているか」を確認する手段ではない。「どのコードが二度と実行されない死体(デッドコード)であるかを暴き、容赦なくコードベースから切除するための外科手術ツール」なのだ。

本稿では、Clangのプロファイル誘導最適化(PGO)インフラストラクチャの深層を活用し、`llvm-cov`と`llvm-profdata`を極限まで使い倒すための実践的アーキテクチャを解説する。

—

1. 内部アーキテクチャの理解:なぜ `gcov` ではなく `llvm-cov` なのか?

従来のGCCエコシステムにおける`gcov`は、コンパイル時に各基本ブロック(Basic Block)にカウンタを埋め込み、実行時にファイルへダンプする仕組みをとっていた。これにはI/Oのオーバーヘッドと、バイナリサイズの肥大化という構造的課題があった。

一方、Clang/LLVMのソースベースコードカバレッジ(Source-based Code Coverage)は、コンパイラバックエンドレベルで抽象構文木(AST)と中間表現(IR)の段階からカバレッジマッピングを生成する。

[C/C++ Source]
↓ (Clang Frontend + -fprofile-instr-generate -fcoverage-mapping)
[LLVM IR]
↓ (Backend Instrumentation)
[Instrumented Binary]
↓ (Execution)
[Raw Profile Data: .profraw]
↓ (llvm-profdata merge)
[Indexed Profile Data: .profdata]
↓ (llvm-cov show / export)
[Visualizable Actionable Insights]

このアーキテクチャの最大のメリットは、バイナリの実行効率をほとんど落とすことなく、正確なブロック単位、さらには条件分岐(Branch Coverage)や関数単位の実行回数をミリ秒単位で抽出できる点にある。

—

2. 実行環境の構築:コンテナ化された完全自動カバレッジパイプライン

アドホックな手元での計測は意味を持たない。開発者が意識することなく、CI/CDパイプライン上で常にカバレッジが計測され、デッドコード率が閾値を超えたらビルドを落とす、あるいはレポートを自動生成する仕組みが必要だ。

ここでは、最新のLLVMツールチェーンを備えたDocker環境をベースに、一切の妥協のないビルド&解析スクリプトを構築する。

2.1. Dockerfile による再現性の担保

業界標準の軽量かつ堅牢なベースイメージ
FROM ubuntu:22.04 AS builder

タイムゾーンの固定と非対話モードの設定
ENV DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

LLVM 18 ツールチェーンおよびビルドツールのインストール
RUN apt-get update && apt-get install -y –no-install-recommends \
lsb-release \
wget \
software-properties-common \
gnupg \
ninja-build \
cmake \
git \
&& wget https://apt.llvm.org/llvm.sh \
&& chmod +x llvm.sh \
&& ./llvm.sh 18 all \
&& rm -rf /var/lib/apt/lists/

デフォルトのコンパイラをClang-18に設定
ENV CC=clang-18
ENV CXX=clang++-18
ENV PATH=”/usr/lib/llvm-18/bin:${PATH}”

WORKDIR /workspace

—

3. 究極のコンパイルフラグチューニング

カバレッジ精度を極限まで高めるためには、最適化レベルとデバッグ情報のバランスが極めて重要である。`-O0`でビルドしたバイナリと、`-O3`で最適化されたバイナリでは、インライン展開や死体コード削除(DCE)によりカバレッジマップの構造が変わる。

本番同等の最適化を維持しつつ、正確なカバレッジを取るためのCMake設定例を示す。

cmake_minimum_required(VERSION 3.22)
project(CoverageTarget CXX)

set(CMAKE_CXX_STANDARD 20)

Clangによるソースベースカバレッジを有効化するフラグ群
-fprofile-instr-generate: プロファイル生成コードをバイナリに埋め込む
-fcoverage-mapping: ソースコード上の位置と中間表現のマッピングを生成する
-fno-use-cxa-atexit: 例外処理や静的デストラクタ周りのノイズを削減(環境により調整)
set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -O3 -fprofile-instr-generate -fcoverage-mapping -g -fno-omit-frame-pointer”)
set(CMAKE_EXE_LINKER_FLAGS “${CMAKE_EXE_LINKER_FLAGS} -fprofile-instr-generate -fcoverage-mapping”)

ターゲットの定義
add_executable(target_app src/main.cpp src/legacy_module.cpp)

テストターゲット
add_executable(unit_tests tests/test_main.cpp src/legacy_module.cpp)

—

4. 自動化スクリプト:プロファイルの収集・マージ・デッドコード抽出

バイナリを実行すると `.profraw` という生データが生成される。これを `llvm-profdata` でインデックス化し、`llvm-cov` を用いて人間が読める、あるいは機械処理可能な形式に変換する。

以下のシェルスクリプトは、テストを実行し、実行されていない「死体コード」の行を完全にあぶり出すためのプロダクションスクリプトである。

!/usr/bin/env bash
set -euo pipefail

環境変数の定義
export LLVM_PROFILE_FILE=”reports/code_coverage-%p-%m.profraw”
BUILD_DIR=”build”
REPORT_DIR=”reports”

mkdir -p “$BUILD_DIR” “$REPORT_DIR”

echo “==> 1. CMake Configuring & Building…”
cmake -S . -B “$BUILD_DIR” -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake –build “$BUILD_DIR” –parallel $(nproc)

echo “==> 2. Running Test Suite…”
テストバイナリを実行し、%p(プロセスID)と%m(ユニークシグネチャ)のprofrawを生成
“$BUILD_DIR/unit_tests”

echo “==> 3. Merging Raw Profiles…”
複数のprofrawファイルをllvm-profdataで高パフォーマンスにマージする
llvm-profdata merge -sparse “$REPORT_DIR”/.profraw -o “$REPORT_DIR/combined.profdata”

echo “==> 4. Exporting Coverage Data to JSON for CI/CD Analysis…”
構造化データとしてエクスポートし、外部ツール(自製ダッシュボード等)での解析を可能にする
llvm-cov export “$BUILD_DIR/unit_tests” \
-instr-profile=”$REPORT_DIR/combined.profdata” \
-format=text > “$REPORT_DIR/coverage_report.json”

echo “==> 5. Generating Human-Readable Text Summary…”
llvm-cov report “$BUILD_DIR/unit_tests” \
-instr-profile=”$REPORT_DIR/combined.profdata” \
-show-branch-coverage

echo “==> 6. Extracting Unexecuted (Dead) Code Paths…”
特定のモジュールにおける実行回数「0」の行を完全抽出する
llvm-cov show “$BUILD_DIR/unit_tests” \
-instr-profile=”$REPORT_DIR/combined.profdata” \
-name-regex=”Legacy.” \
-line-coverage-gt=0 \
> “$REPORT_DIR/dead_code_audit.txt”

echo “==> Analysis Complete. Check $REPORT_DIR for outputs.”

—

5. 実務的ハック:実行回数「0」のデッドコードを自動検出し、Gitから抹消する

ここからが真のDevOpsアーキテクトの腕の見せ所である。生成されたカバレッジレポート(JSON)をパースし、「過去6ヶ月間一度も実行されておらず、かつ特定のモジュールに含まれる関数」を特定し、自動でチケット起票あるいはコード削除のプルーフとするPythonスクリプトをCIに組み込む。

!/usr/bin/env python3
import json
import sys

def audit_dead_code(json_path):
try:
with open(json_path, ‘r’) as f:
data = json.load(f)
except Exception as e:
print(f”Error loading coverage JSON: {e}”)
sys.exit(1)

dead_functions = []

# llvm-cov exportのJSON構造を走査
for rect in data.get(‘data’, []):
for file_info in rect.get(‘files’, []):
file_name = file_info.get(‘filename’)

# 外部ライブラリやテストコードは除外してターゲットを絞る
if “src/legacy” not in file_name:
continue

for function in file_info.get(‘functions’, []):
func_name = function.get(‘name’)
execution_count = function.get(‘execution_count’)

# 実行回数が完全に「0」の関数を特定
if execution_count == 0:
dead_functions.append({
“file”: file_name,
“function”: func_name,
“regions_count”: len(function.get(‘regions’, []))
})

print(f”=== DEAD CODE AUDIT REPORT ===”)
print(f”Found {len(dead_functions)} unexecuted functions in legacy modules.\n”)

for item in dead_functions:
print(f”[DEAD] File: {item[‘file’]} | Function: {item[‘function’]}”)

# 完全に死んでいるコードが一定数を超えたらCIを警告(あるいは失敗)させる
if len(dead_functions) > 0:
print(“\nAction Required: These functions are candidates for immediate removal.”)
# sys.exit(2) # 必要に応じてCIをブロック
else:
print(“\nClean codebase! No dead functions detected in target modules.”)

if __name__ == “__main__”:
if len(sys.argv) < 2: print("Usage: python3 audit_dead_code.py “)
sys.exit(1)
audit_dead_code(sys.argv[1])

—

6. パフォーマンスとメモリ消費の最適化ハック

巨大なコードベース(数百万行規模のC++プロジェクト)において、`llvm-cov`の実行はメモリ食い虫となり、CIランナーがOOM (Out of Memory) Killerに屠られることが多々ある。これを回避するためのエキスパート向けハックを授ける。

1. `-sparse` フラグの徹底活用:
`llvm-profdata merge` を行う際は、必ず `-sparse` オプションを付与すること。これにより、実行されなかった膨大な数のゼロカウンタ領域がインデックスから省かれ、ファイルサイズとメモリ消費量を劇的に削減できる。
2. 対象ファイルの絞り込み (`-object` / `-sources`):
巨大なモノリスバイナリに対して全体を `llvm-cov show` しようとすると、数十GBのメモリを消費してクラッシュする。解析時は `-sources` オプションで今回変更があったディレクトリや、監査対象のサブモジュールパスを明示的に指定せよ。
3. 並列処理とインクリメンタルビルドの分離:
CIキャッシュ戦略において、`.profraw` の生成元となるオブジェクトファイルとプロファイルデータのキャッシュキーを適切に分離し、変更のないテストケースの再実行コストを極小化する。

—

結び:コードを削る勇気を持つために

カバレッジ解析ツールを「テストの合格証」として使う時代は終わった。
`llvm-cov` と `llvm-profdata` をパイプラインの血流に組み込み、コンパイラバックエンドの緻密な計測データをハックすることで、あなたの組織は「肥大化し続ける恐怖のレガシーシステム」から脱却することができる。

動かないコードを愛おしむな。データをもって証明し、迷わず削除せよ。それこそが、最高峰の開発環境アーキテクトがたどり着くべき、美しく強靭なコードベースの姿である。

タイトルとURLをコピーしました