Go と C++ の可変長引数

# 動機

go の syscall パッケージには、

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

# 結果

```text
# 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++ の可変長引数はノーコスト。
`fixed` や `vari(F)` の呼び方だとインライン展開されないのでちょっと遅くて、 `fixed(L)` や `vari(L)` の呼び方だとインライン展開されるので速い。可変長引数かどうかではなく、インライン展開されるかどうかが重要。

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