- Передавать структуру, содержащую все необязательные аргументы в полях:
funcStructOpts(Opts{p1: 1, p2: 2, p8: 8, p9: 9, p10: 10})
- Способ предложенный Робом Пайком с использованием функциональных аргументов:
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:
Всё логично, время тратится на выделение памяти под анонимные функции, а как известно
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
Выводы
Для себя я так и не решил, что же лучше. Предлагаю похоливарить в комментариях, а для сбора статистики опрос ниже.
Комментарии (14)
Scratch
10.12.2017 23:59Используем метод Роба Пайка, он довольно удобен в повседневном использовании. В плане производительности поисками таких вот блох обычно занимаются, когда всё остальное в проекте уже вылизано до блеска (т.е. никогда). Всегда находятся другие, более очевидные места, где можно подкрутить производительность
khim
11.12.2017 19:51Это всё психология. Тот факт, что «всегда находятся другие, более очевидные места, где можно подкрутить производительность» просто-напросто обозначает, что 90% всех ресурсов ваша программа тратит просто на нагрев воздуха.
Если писать сразу с учётом всех этих «краевых» эффектов, то можно, как правило, создать программу, которая будет в 5-10 раз быстрее, но оно вам надо? Как правило ответ — «нет, не надо», возможность быстро менять программу при изменении требований важнее.
Но, собственно, Go на выжимание «всех соков» из процессора и не претендует — для этого C++ есть (хотя, в последнее время, rust начал на ту же нишу претендовать).
ngalayko
11.12.2017 04:47функциями можно описать куда больший набор опций для конечной структуры, чем просто список параметров, а такими мелкими оптимизациями заниматься чаще всего бесполезно.
vesper-bot
11.12.2017 09:41Лишний malloc на такую мелочь — вредно. По мне, любой синтаксический сахар должен писаться так, чтобы даже если весь код будет его использовать вместо оптимизированной версии, потери производительности были бы достаточно малы, чтобы можно было ими пренебречь, выиграв в скорости разработки. И я сильно сомневаюсь, что этот «сахар» его вообще даст.
khim
11.12.2017 19:54Не путайте C++ и Go/Java/PHP/JavaScript/etc. Это в C++ стараются сделать «сахар» таким, чтобы компилятор мог его полностью «растворить». В другия языках часто «сахар» замедляет исполнение в десятки раз — но люди с этим мирятся ради гибкости.
vesper-bot
12.12.2017 09:38Я учился программировать во времена, когда не во всяком ПК было 64 КБ памяти, поэтому для меня сегодняшние тенденции в наворачивании фреймворков на фреймворки ради скорости разработки — страшное расстройство. Другое дело, что скорость разработки нынче действительно требуется огромная, потому что конкурентов под каждым забором по пятеро, и все делают что-то вроде твоей программы, и кто первым выпустил, тот и победил, даже если в программе сделан только green path. Печально, но такова жизнь.
andboson
11.12.2017 21:57У вас что-то не так в коде, если вам нужно 10 аргументов в функции, тем более, не обзательных.
ЗЫ: не очень понятна постановка вопроса вообще, почему именно функциональные аргументы? Как же простота?
svistunov Автор
11.12.2017 22:01Например метод Dial из библиотеки grpc имеет почти 30 аргументов: godoc.org/google.golang.org/grpc#DialOption
el777
11.12.2017 22:05Интересная статья одного из "столпов" Go — Dave Cheney https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis где он приводит третий вариант и подробно все разбирает с плюсами и минусами.
vesper-bot
12.12.2017 09:45Вроде как «третий вариант» это примерно то же, что в этой статье вариант с функциями, только функции у него принимают в первом аргументе этакий this для изменения, но при этом не находятся в кодовом пространстве класса, который призваны изменять. Да и не отражены там вопросы производительности вообще никак, только вопросы расширяемости, и в условиях динамически расширяемого API функции от this выглядят действительно удобнее — при добавлении фичи не нужно переписывать структуру *opts и код конструктора, а достаточно написать функцию для конфигурирования конкретного параметра.
PS: а что, если передавать в такой конструктор функцию от (*класс, ...int) или в крайнем случае ...string? Ещё гибче получается, причем второй аргумент функции оказывается опциональным списком — надо тебе, чтобы у фичи было много параметров, все запихиваешь в строки и передаешь, надо ноль — пишешь функцию от одного аргумента *класс, и хватит.
Foxcool
Сахарок на мой взгляд выглядит скорее запутаннее, к тому же. Этот стек вызовов функций в собственной голове переполняется ради понимания примитивного процесса получения аргументов (:
Symphel
Синтаксический сахар вроде должен уменьшать количество кода и делать его проще, а тут как раз наоборот.
khim
Ну уж нет. Синтаксический сахар почти всегда увеличивает количества кода. Вот количество текста в исходном коде — он должен уменьшать… и уменьшает…