Необязательные аргументы в функциях Go

в 19:04, , рубрики: benchmark, Go

В Go нет синтаксиса для определения необязательных аргументов в функциях, поэтому приходится использовать обходные пути. Я знаю 2:

  1. Передавать структуру, содержащую все необязательные аргументы в полях:
    funcStructOpts(Opts{p1: 1, p2: 2, p8: 8, p9: 9, p10: 10})

  2. Способ предложенный Робом Пайком с использованием функциональных аргументов:
    funcWithOpts(WithP1(1), WithP2(2), WithP8(8), WithP9(9), WithP10(10))

Второй способ в принципе делает тоже самое, но с синтаксическим сахаром. Мне не давала покоя мысль, а сколько же стоит этот сахар, кому ещё интересно прошу под кат.

Для тестов я использовал структуру с 10 опциями:

type Opts struct {
        p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 int
}

и 2 пустые функции:

func funcStructOpts(o Opts) {
}

func funcWithOpts(opts ...OptsFunc) {
        o := &Opts{}
        for _, opt := range opts {
                opt(o)
        }
}

Для тех, кто не работал с функциональными аргументами немного расскажу как они работают. Каждая опция описывается в виде функции, которая возвращает функцию, которая изменяет структуру с параметрами, например:

func WithP1(v int) OptsFunc {
        return func(opts *Opts) {
                opts.p1 = v
        }
}

где OptsFunc — это type OptsFunc func(*Opts)
При вызове функции их передают в качестве аргументов, а внутри функции в цикле заполняют структуру с аргументами:

o := &Opts{}
for _, opt := range opts {
    opt(o)
}

Здесь магия и заканчивается, теперь у нас есть заполненная структура, осталось только выяснить, сколько стоит сахар. Для этого я написал простой benchmark:

func BenchmarkStructOpts(b *testing.B) {
        for i := 0; i < b.N; i++ {
                funcStructOpts(Opts{
                        p1:  i,
                        p2:  i + 2,
                        p3:  i + 3,
                        p4:  i + 4,
                        p5:  i + 5,
                        p6:  i + 6,
                        p7:  i + 7,
                        p8:  i + 8,
                        p9:  i + 9,
                        p10: i + 10,
                })
        }
}

func BenchmarkWithOpts(b *testing.B) {
        for i := 0; i < b.N; i++ {
                funcWithOpts(WithP1(i), WithP2(i+2), WithP3(i+3), WithP4(i+4), WithP5(i+5), WithP6(i+6), WithP7(i+7),
                        WithP8(i+8), WithP9(i+9), WithP10(i+10))
        }
}

Для тестирования я использовал Go 1.9 на Intel® Core(TM) i7-4700HQ CPU @ 2.40GHz. Результаты:
BenchmarkStructOpts-8 100000000 10.7 ns/op 0 B/op 0 allocs/op
BenchmarkWithOpts-8 3000000 399 ns/op 240 B/op 11 allocs/op

Результаты противоречивые, с одной стороны разница почти в 40 раз, с другой — это сотни наносекунд.

Мне стало интересно, а на что же тратится время, ниже вывод pprof:

Необязательные аргументы в функциях Go - 1

Всё логично, время тратится на выделение памяти под анонимные функции, а как известно malloc — это время, много времени…

Для чистоты эксперимента я проверил, что происходит при вызове без аргументов:

func BenchmarkEmptyStructOpts(b *testing.B) {
        for i := 0; i < b.N; i++ {
                funcStructOpts(Opts{})
        }
}

func BenchmarkEmptyWithOpts(b *testing.B) {
        for i := 0; i < b.N; i++ {
                funcWithOpts()
        }
}

Здесь разница немного меньше, примерно в 20 раз:
BenchmarkEmptyStructOpts-8 1000000000 2.75 ns/op 0 B/op 0 allocs/op
BenchmarkEmptyWithOpts-8 30000000 57.0 ns/op 80 B/op 1 allocs/op

Выводы

Для себя я так и не решил, что же лучше. Предлагаю похоливарить в комментариях, а для сбора статистики опрос ниже:

Автор: Свистунов Сергей

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js