Matplotlib을 이용한 Google Benchmark 결과 시각화

· by 박승재

Matplotlib은 파이썬에서 데이터 시각화에 사용하는 대표적인 라이브러리입니다.

파이썬과 Matplotlib을 이용하면 쉽고 빠르게 원하는 데이터를 그래프로 나타낼 수 있습니다.

Google Benchmark는 C++ 코드의 성능을 측정하는 라이브러리로, 사용하면 아래와 같은 결과를 얻을 수 있습니다.

-----------------------------------------------------------------
Benchmark                       Time             CPU   Iterations
-----------------------------------------------------------------
BM_BubbleSort/1              3432 ns         3434 ns       203943
BM_BubbleSort/8              3643 ns         3643 ns       192168
BM_BubbleSort/64            15190 ns        15181 ns        46107
BM_BubbleSort/512          558533 ns       558107 ns         1254
BM_BubbleSort/4096       49870593 ns     49837304 ns           14
BM_BubbleSort/32768    3792011976 ns   3789209799 ns            1
BM_HeapSort/1                3444 ns         3446 ns       203316
BM_HeapSort/8                3692 ns         3696 ns       189640
BM_HeapSort/64               6986 ns         6984 ns       100220
BM_HeapSort/512             43565 ns        43501 ns        16099
BM_HeapSort/4096           418680 ns       418439 ns         1673
BM_HeapSort/32768         4894845 ns      4891226 ns          143
BM_InsertionSort/1           3426 ns         3430 ns       203730
BM_InsertionSort/8           3723 ns         3726 ns       189032
BM_InsertionSort/64          8438 ns         8435 ns        82914
BM_InsertionSort/512        82394 ns        82298 ns         8509
BM_InsertionSort/4096     2099952 ns      2098425 ns          334
BM_InsertionSort/32768  128825712 ns    128714670 ns            5
BM_MergeSort/1               3440 ns         3444 ns       203974
BM_MergeSort/8               4269 ns         4270 ns       163778
BM_MergeSort/64             12766 ns        12763 ns        54769
BM_MergeSort/512            94096 ns        94026 ns         7445
BM_MergeSort/4096          851189 ns       850581 ns          822
BM_MergeSort/32768        7766578 ns      7758767 ns           90
BM_QuickSort/1               3442 ns         3445 ns       204311
BM_QuickSort/8               3755 ns         3757 ns       186077
BM_QuickSort/64              8495 ns         8491 ns        82482
BM_QuickSort/512            59769 ns        59692 ns        11751
BM_QuickSort/4096          579835 ns       579384 ns         1208
BM_QuickSort/32768        5575984 ns      5571140 ns          126
BM_SelectionSort/1           3450 ns         3453 ns       203011
BM_SelectionSort/8           3554 ns         3558 ns       197000
BM_SelectionSort/64          8427 ns         8422 ns        83086
BM_SelectionSort/512       273484 ns       273251 ns         2562
BM_SelectionSort/4096    17377663 ns     17363755 ns           40
BM_SelectionSort/32768 1101676702 ns   1100820273 ns            1

C++ Sort 함수의 성능 비교 표

Google Benchmark로는 표로 된 결과를 얻을 수 있습니다.

이것을 배열 크기-실행 시간 그래프로 나타내면 어떤 알고리즘이 효율적인지 한눈에 파악하기 쉬울 것입니다.

위와 같은 표 형태보다 JSON 형태가 파싱이 편리하기 때문에, 우선 Benchmark 결과를 JSON으로 출력해봅시다.

파싱(Parsing)은 어떤 데이터에서 원하는 데이터를 특정 패턴이나 순서로 추출해 가공하는 것을 말합니다.

Google Benchmark에서는 BENCHMARK_FORMATBENCHMARK_OUT을 이용해 출력되는 포맷(Format)과 파일을 지정할 수 있습니다.

Benchmark 파일을 실행할 때, 환경변수로 넣어줍니다.

$ BENCHMARK_FORMAT=json BENCHMARK_OUT=benchmark.json build/benchmarks/benchmark_sort

실행이 끝나면, 결과는 JSON으로 benchmark.json(BENCHMARK_OUT)에 출력됩니다.

benchmark.json:

{
  "context": {
    "date": "2021-11-07T11:54:01+09:00",
    "host_name": "ubuntu",
    "executable": "build/benchmarks/benchmark_sort",
    "num_cpus": 4,
    "mhz_per_cpu": 1500,
    "cpu_scaling_enabled": true,
    "caches": [
    ],
    "load_avg": [0,0,0],
    "library_build_type": "release"
  },
  "benchmarks": [
    {
      "name": "BM_BubbleSort/1",
      "run_name": "BM_BubbleSort/1",
      "run_type": "iteration",
      "repetitions": 0,
      "repetition_index": 0,
      "threads": 1,
      "iterations": 204332,
      "real_time": 3.4235093321168374e+03,
      "cpu_time": 3.4276080741147057e+03,
      "time_unit": "ns"
    },
    {
      "name": "BM_BubbleSort/8",
      "run_name": "BM_BubbleSort/8",
      "run_type": "iteration",
      "repetitions": 0,
      "repetition_index": 0,
      "threads": 1,
      "iterations": 190606,
      "real_time": 3.6746951781538737e+03,
      "cpu_time": 3.6777019348813947e+03,
      "time_unit": "ns"
    },
    ...
  ]
}

이제 JSON 파일을 읽어봅시다.

with open('benchmark.json') as file:
    benchmark_result = json.load(file)
    benchmarks = benchmark_result['benchmarks']

JSON 파일에서 그래프에서 사용될 레이블(Label)과 X축(배열 크기)를 추출하는 함수를 정의합니다.

이때, X축을 1, 8, 64, 512, … 형태로 커지게 하기 위해 문자열(str) 타입으로 정의합니다.

def extract_label_from_benchmark(benchmark):
    benchmark_name = benchmark['name']
    return benchmark_name.split('/')[0][3:]  # erase `BM_`

def extract_size_from_benchmark(benchmark):
    benchmark_name = benchmark['name']
    return benchmark_name.split('/')[1]  # not int, for exp scale x axis

itertoolsgroupby를 이용해 실행 결과를 알고리즘 종류별로 묶어줍니다.

elapsed_times = groupby(benchmarks, extract_label_from_benchmark)

이제 반복문을 돌면서, 알고리즘 종류 별로 배열 크기와 실행 시간을 종합해 그래프에 그립니다.

Y축(실행 시간)이 너무 커질 경우를 대비해, math.log로 실행 시간에 \(ln\)을 취해줍니다.

for key, group in elapsed_times:
    benchmark = list(group)
    x = list(map(extract_size_from_benchmark, benchmark))
    y = list(map(operator.itemgetter('real_time'), benchmark))
    log_y = list(map(math.log, y))
    plt.plot(x, log_y, label=key)

group은 제너레이터(Generator)이기 때문에 재사용이 불가능합니다. 따라서 재사용이 필요한 경우, list원소를 복사해 사용해야 합니다.

참고: 파이썬 Generator(제네레이터)

마지막으로 그래프에 축 제목과 범례를 추가하고 이미지 파일로 저장합니다.

plt.xlabel('array size')
plt.ylabel('ln(time)')
plt.title('Sorting Algorithm Benchmark')
plt.legend()
plt.savefig('benchmark.png')
plt.savefig('benchmark.svg')

Benchmark

benchmark.svg

최종 스크립트 파일입니다.

ArgumentParser를 이용해 임의의 결과 파일에 대해 시각화를 하는 기능도 추가했습니다.

from argparse import ArgumentParser
from itertools import groupby
import json
import math
import operator
import matplotlib.pyplot as plt


def extract_label_from_benchmark(benchmark):
    benchmark_name = benchmark['name']
    return benchmark_name.split('/')[0][3:]  # erase `BM_`


def extract_size_from_benchmark(benchmark):
    benchmark_name = benchmark['name']
    return benchmark_name.split('/')[1]  # not int, for exp scale x axis


if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument('path', help='benchmark result json file')
    args = parser.parse_args()

    with open(args.path) as file:
        benchmark_result = json.load(file)
        benchmarks = benchmark_result['benchmarks']
        elapsed_times = groupby(benchmarks, extract_label_from_benchmark)
        for key, group in elapsed_times:
            benchmark = list(group)
            x = list(map(extract_size_from_benchmark, benchmark))
            y = list(map(operator.itemgetter('real_time'), benchmark))
            log_y = list(map(math.log, y))
            plt.plot(x, log_y, label=key)
        plt.xlabel('array size')
        plt.ylabel('ln(time)')
        plt.title('Sorting Algorithm Benchmark')
        plt.legend()
        plt.savefig('benchmark.png')
        plt.savefig('benchmark.svg')