LoginSignup
3
1

More than 3 years have passed since last update.

Go と C++ の可変長引数

Posted at

動機

go の syscall パッケージには、

go
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

のように、引数の数に応じて関数が用意されている。

go には可変長引数という素晴らしい仕組みがあるのになぜ? もしかして可変長引数遅いの? と思って今日も楽しいマイクロベンチマーク。

ソースコード

go

まあソースコードから。

go
package main

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

func vari6(n ...uintptr) uintptr {
    return n[0] + n[1] + n[2] + n[3] + n[4] + n[5]
}

func fixed6(a, b, c, d, e, f uintptr) uintptr {
    return a + b + c + d + e + f
}

func benchV(title string, num uintptr, proc func(n ...uintptr) uintptr) {
    t0 := time.Now()
    sum := uintptr(0)
    for i := uintptr(0); i < num; i++ {
        sum += proc(i&1, i&2, i&4, i&8, i&16, i&32)
    }
    t1 := time.Now()
    fmt.Println(title, t1.Sub(t0), sum)
}

func benchF(title string, num uintptr, proc func(a, b, c, d, e, f uintptr) uintptr) {
    t0 := time.Now()
    sum := uintptr(0)
    for i := uintptr(0); i < num; i++ {
        sum += proc(i&1, i&2, i&4, i&8, i&16, i&32)
    }
    t1 := time.Now()
    fmt.Println(title, t1.Sub(t0), sum)
}

func main() {
    num, err := strconv.ParseInt(os.Args[1], 10, 64)
    if err != nil {
        panic(err)
    }
    for i := 0; i < 3; i++ {
        benchV("vari", uintptr(num), vari6)
        benchF("fixed", uintptr(num), fixed6)
    }
}

vari6 を渡すための benchV と、 fixed6 を渡すための benchF
全く同じ内容なのに別の定義になっているのが無様だが仕方ない。

c++

ついでに、 go とは全く異なる仕組みで可変長引数を実現している C++ 版も

c++17
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iostream>

using std_clock = std::chrono::high_resolution_clock;

std::uintptr_t fixed6(std::uintptr_t a, std::uintptr_t b, std::uintptr_t c,
                      std::uintptr_t d, std::uintptr_t e, std::uintptr_t f) {
  return a + b + c + d + e + f;
}

std::uintptr_t vari() { return 0; }

template <typename... inttypes> //
std::uintptr_t vari(std::uintptr_t v0, inttypes... rest) {
  return v0 + vari(rest...);
}

template <typename proc_t>
void bench(char const *title, std::uintptr_t num, proc_t proc) {
  auto t0 = std_clock::now();
  std::uintptr_t sum = 0;
  for (std::uintptr_t i = 0; i < num; ++i) {
    sum += proc(i & 1, i & 2, i & 4, i & 8, i & 16, i & 32);
  }
  auto t1 = std_clock::now();
  auto diff_us =
      std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count();
  std::cout << title << " " << (diff_us * 1e-6) << "ms " << sum << std::endl;
}

int main(int argc, char const *argv[]) {
  std::uintptr_t num = argc < 2 ? 100 : std::atoi(argv[1]);
  using u = std::uintptr_t;
  for (std::uintptr_t i = 0; i < 3; ++i) {
    bench("fixed", num, fixed6);
    bench("fixed(L)", num, [](u a, u b, u c, u d, u e, u f) -> u {
      return fixed6(a, b, c, d, e, f);
    });
    bench("vari(F)", num, vari<u, u, u, u, u>);
    bench("vari(L)", num, [](u a, u b, u c, u d, u e, u f) -> u {
      return vari(a, b, c, d, e, f);
    });
  }
}

go より数行短くなっている。
C++ の場合、可変長引数関数は関数ではなく関数テンプレートなので別の関数にそのまま渡すことができない(できるかもしれなけど、やり方がわからなかった...)。
そこで。
引数の数と型をテンプレート引数で指定したり、ラムダに埋め込んだりしてみた。

結果

# go
./main 10000000 | tail -2
vari 344.428796ms 315000000
fixed 40.811772ms 315000000

# C++(clang)
./a.clang.out  10000000 | tail -4
fixed 21.4052ms 315000000
fixed(L) 1.68218ms 315000000
vari(F) 21.3051ms 315000000
vari(L) 1.80019ms 315000000

# C++(g++-9)
./a.gcc9.out  10000000 | tail -4
fixed 24.349ms 315000000
fixed(L) 3.9ms 315000000
vari(F) 24.405ms 315000000
vari(L) 3.75ms 315000000

なんか go と C++ で順序が違うけど気にしない。

まとめ

どうも、go の可変長引数は遅そうな感じ。
手元のマシンで一回当たり、30ナノ秒ぐらいかかっている模様。

一方。C++ の可変長引数はノーコスト。
fixedvari(F) の呼び方だとインライン展開されないのでちょっと遅くて、 fixed(L)vari(L) の呼び方だとインライン展開されるので速い。可変長引数かどうかではなく、インライン展開されるかどうかが重要。

あと。関数テンプレート vari__attribute__((noinline)) を付けるとだいぶ遅くなる。再帰呼び出しが全部本当に関数呼び出しに展開されてしまうので仕方ない。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1