15 марта 2022 года. Был морозный весенний день. Ветер старался доказать, что он не промах и залезть под куртки, кофты и прочие принадлежности гардероба, чтобы из первых рук, куда уж придется, принести весеннее настроение через свежесть. Не очень-то у него это получалось. Причем при любом раскладе. Если попадалась хорошая куртка и не пускала незваного гостя - ветру рассказать о весне не получалось. Если же удавалось забраться за шиворот или пройтись ледяным дыханием свежести по пузу - этого уже не понимал прохожий. Кутался еще сильнее и поскорее старался уйти от этого весеннего настроения. Но это была не единственная неоднозначность. Именно 15-го марта в мир была превнесена еще одна неоднозначность, спровоцировавшая жаркие споры - релиз golang 1.18 вместе с системой generic-ов.

Сама по себе концепция дженериков не нова. Самый известный приход этой концепции в мир - java великая и ужасная. Сама конецепция понятна и даже логична в появлении. На мой взгляд главное, чего должны были достигнуть дженерики - убрать копипасту кода. Дженерики появились, позволяя писать теперь унифицированные функции вместо двух или трех под разные типы данных, но с одинаковыми операциями. Вроде проблема решена, все радуются, со всех концов слышен смех, вверх взлетают шляпы, толпа ликует, в небе разрываются салюты! Или не совсем? Для того, чтобы сломя голову бросаться решать проблему, сама проблема должна быть, а была ли она?

Много где при обьяснении дженериков я встречал реализацию функции, суммирующей разные числа - int, float32, float64. Да, пример показывает, что такое generic, проблема копипасты решена. Но как часто вы пишете функцию для сложения 2, 3 или даже 4 чисел? Все эти примеры оставляют терпкое послевкусие обмана. Сначала завлекают крупными лозунгами, мол Breaking News, прорыв года, все сюда, ваша жизнь не станет прежней! А открываешь статью и там тебе рассказывают, как сложить 2+2 (действительно, моя жизнь не станет прежней - больше не буду верить заголовкам статей!).

А если посмотреть на что-то более практическое? Мне очень понравилась идея попробовать дженерики на стеке (тем более в тот момент мне он понадобился в небольшом личном проекте). Это достаточно простая для понимания и реализации структура. На мой взгляд это идеальный подопытный для экспериментов. При любом раскладе тот или иной вариант пригодится в хозяйстве. Изначально я взял готовую библиотеку, но потом решил сравнить другую реализацию. Спойлер: библиотеку переписал на дженерики и стал использовать в своем проекте. А сравнение производительности двух решений натолкнуло меня на эту статью.

Давайте глянем на простейшую реализацию стека. Если код удобнее видеть на отдельной вкладке - исходники находятcя тут.

Реализация через интерфейсы
package main

type (
	iNode struct {
		val  interface{}
		next *iNode
	}
	InterfaceStack struct {
		top *iNode
		len int
	}
)

func NewInterfaceStack() *InterfaceStack {
	return &InterfaceStack{}
}

func (istack *InterfaceStack) Push(val interface{}) {
	var n iNode = iNode{val: val, next: istack.top}
	istack.len += 1
	istack.top = &n
}

func (istack *InterfaceStack) Pop() interface{} {
	if istack.len <= 0 {
		return nil
	}
	istack.len -= 1
	var n *iNode = istack.top
	istack.top = n.next
	return n.val
}

func (istack *InterfaceStack) Peak() interface{} {
	if istack.len <= 0 {
		return nil
	}
	return istack.top.val
}

func (istack *InterfaceStack) Len() int {
	return istack.len
}

Реализация через дженерики
package main

type (
	gNode[NT any] struct {
		val  NT
		next *gNode[NT]
	}
	GenericStack[ST any] struct {
		top *gNode[ST]
		len int
	}
)

func NewGenericStack[GS any]() *GenericStack[GS] {
	return &GenericStack[GS]{}
}

func (gstack *GenericStack[ST]) Push(val ST) {
	var n gNode[ST] = gNode[ST]{val: val, next: gstack.top}
	gstack.len += 1
	gstack.top = &n
}

func (gstack *GenericStack[ST]) Pop() (res ST, exists bool) {
	if gstack.len <= 0 {
		exists = false
		return
	}
	gstack.len -= 1

	var n *gNode[ST] = gstack.top
	gstack.top = n.next
	return n.val, true
}

func (gstack *GenericStack[ST]) Peak() (res ST, exists bool) {
	if gstack.len <= 0 {
		exists = false
		return
	}
	return gstack.top.val, true
}

func (gstack GenericStack[ST]) Len() int {
	return gstack.len
}

Принципиальных различий нет. Единственное, что дженериках функции Pop и Peak возвращают два аргумента вместо одного.

А теперь помотрим на использование
package main

import "fmt"

func main() {
	// Interface
	istack := NewInterfaceStack()
	istack.Push(12)
	istack.Push(32)
	ival := istack.Pop();
	if ival != nil{
		if val, ok := ival.(int); !ok {
			panic("wrong type in interface stack")
		} else {
			fmt.Printf("Got '%v' from interface stack\n", val)
		}
    }
  
	// Generic
	gstack := NewGenericStack[int]()
	gstack.Push(54)
	gstack.Push(67)
	if val, exists := gstack.Pop(); exists {
		fmt.Printf("Got '%v' from generic stack\n", val)
	}
}

Пример кода есть (и он не просто компилируется, но еще и запускается без ошибок!) так что можно включать режим диванного критика и пройтись по всем аспектам этих двух подходов.

Реализация через интерфейсы. Универсальна под любой тип. Более того, она позволяет хранить разные объекты в одном стеке (особенно актуально, если у вас завалялись одна или две лишние целые ноги, по которым неплохо было бы пострелять). И размер бинарника будет на пару сотен байт меньше, по сравнению с дженериками. А что касается минусов - необходимо постоянно делать преобразование типов, внимательно следить за тем, что кладем в стек, продумывать обработку ошибок. Есть ненулевая вероятность, что ошибку преобразования придется обрабатывать на уровне выше, значит код будет запутаннее. А еще при рефакторинге можно позабыть поменять преобразование типов в каком-либо месте и долго искать откуда валится ошибка.

А теперь пришёл черед дженериков. Мы изначально знаем какой тип, поэтому преобразовывать ничего не нужно, если где-то попытаемся положить в стек неподходящий тип - компилятор надает по рукам. Меньше кода при использовании. Если нам понадобятся стеки нескольких типов, то создание кода будет переложено на компилятор и не потребует дополнительных усилий со стороны разработчика. Из минусов - теперь надо явно проверять наличие элемента в стеке - либо через длину, либо через возвращаемый флаг.

Мне кажется наш диванный аналитик подсуживает дженерикам! Почитать дак прямо идеальная фича. Но это всё касалось только стилистики кода. Но что же у нас есть еще для сравнения? Производительность! На просторах бескрайнего интернета очень часто в аргументации “за дженерики” аргументируют производительностью. Раз нет необходимости преобразования типов, то работать будет быстрее. И если с читабельностью кода всё действительно понятно - дженерики тут выигрывают прозрачностью использования и меньшим количеством кода, то с производительностью всё не так однозначно. Обычно ограничиваются умозаключениями: раз меньше кода исполняется, то работает производительнее. Да, логика понятна, нативна и производительность действительно зависит от количества выполняемых операций. Но вот насколько быстрее? Собственно для ответа на этот вопрос и была сделана такая простая реализация стека. Если вдруг вам потребуется полная версия, то на дженериках лежит тут , а на интерфейсах лежит тут.

Ну а теперь сами тесты. (код по прежнему можно найти тут, но там только последняя версия - отличается от кода в спойлерах немного).

код тестов интерфейсного подхода
package main

import "testing"

type iTestNode struct {
	val		int
}

func createINode(value int) iTestNode {
	return iTestNode{value}
}

func BenchmarkInterfaceSimpleType(b *testing.B) {
	st := NewInterfaceStack()
	val := 1
	for i := 0; i < b.N; i += 1 {
		st.Push(val)
		if _, ok := st.Pop().(int); !ok {
			panic("Wrong type of data in stack")
		}
	}
}

func BenchmarkInterfaceSimpleCustomType(b *testing.B) {
	st := NewInterfaceStack()
	node := createINode(12)
	for i := 0; i < b.N; i += 1 {
		st.Push(node)
		if _, ok := st.Pop().(iTestNode); !ok {
			panic("Wrong type of data in stack")
		}
	}
}

func BenchmarkInterfaceCustomTypePointer(b *testing.B) {
	st := NewInterfaceStack()
	node := createINode(12)
	for i := 0; i < b.N; i += 1 {
		st.Push(&node)
		if _, ok := st.Pop().(*iTestNode); !ok {
			panic("Wrong type of data in stack")
		}
	}
}

package main

import "testing"

type gTestNode struct {
UserId int64
UserName string
AccessLevel int
Telegram string
Phone string
Skype string
Slack string
Blog string
Instagram string
Facebook string
Twitter string
Avatar []byte
Status string
}

func createGNode() gTestNode {
return gTestNode{
UserId: 12,
UserName: "someuser",
AccessLevel: 99,
Telegram: @someuserr",
Phone: "123456789",
Skype: "someUser",
Slack: "someuser",
Blog: "",
Instagram: @instasomeuserr",
Facebook: "facebook.com/someuser",
Twitter: "twitter.com/someuser",
Avatar: make([]byte, 0),
Status: "ONLINE",
}
}

//go:noinline
func BenchmarkGenericSimpleType(b *testing.B) {
b.StopTimer()
st := NewGenericStackstring
val := "some string for tests"
b.StartTimer()
for i := 0; i < b.N; i += 1 {
st.Push(val)
st.Pop()
}
}

//go:noinline
func BenchmarkGenericCustomType(b *testing.B) {
b.StopTimer()
st := NewGenericStackgTestNode
node := createGNode()
b.StartTimer()
for i := 0; i < b.N; i += 1 {
st.Push(node)
st.Pop()
}
}

//go:noinline
func BenchmarkGenericCustomTypePointer(b *testing.B) {
b.StopTimer()
st := NewGenericStack*gTestNode
node := createGNode()
b.StartTimer()
for i := 0; i < b.N; i += 1 {
st.Push(&node)
st.Pop()
}
}

код тестов подхода на дженериках
package main

import "testing"

type gTestNode struct {
	val      int64
}

func createGNode(value int) gTestNode {
	return gTestNode{value}
}

func BenchmarkGenericSimpleType(b *testing.B) {
	st := NewGenericStack[int]()
	val := 1
	for i := 0; i < b.N; i += 1 {
		st.Push(val)
		st.Pop()
	}
}

func BenchmarkGenericCustomType(b *testing.B) {
	st := NewGenericStack[gTestNode]()
	node := createGNode()
	for i := 0; i < b.N; i += 1 {
		st.Push(node)
		st.Pop()
	}
}

func BenchmarkGenericCustomTypePointer(b *testing.B) {
	st := NewGenericStack[*gTestNode]()
	node := createGNode()
	for i := 0; i < b.N; i += 1 {
		st.Push(&node)
		st.Pop()
	}
}

Машина, на которой проводились тесты

CPU: 8-core AMD Ryzen 7 4700U with Radeon Graphics (-MCP-)
speed/min/max: 1482/1400/2000 MHz Kernel: 5.15.85-1-MANJARO x86_64
Mem: 5500.4/31499.2 MiB (17.5%)
inxi: 3.3.24

Запускать буду не на количество итераций, а на время прохождения теста (т.е. через -benchtime=20s ). Сам код запуска тестов будет таким: go test -bench=. -benchtime=20s. Все тесты буду запускать по 5 раз, чтобы определить порядок.

Результаты
goos: linux
goarch: amd64
pkg: github.com/HoskeOwl/goSimpleStack
cpu: AMD Ryzen 7 4700U with Radeon Graphics         
BenchmarkGenericSimpleType-8                    313764921               77.92 ns/op
BenchmarkGenericCustomType-8                    317463438               71.97 ns/op
BenchmarkGenericCustomTypePointer-8             218245879              111.0 ns/op
BenchmarkInterfaceSimpleType-8                  213324286              113.0 ns/op
BenchmarkInterfaceSimpleCustomType-8            222740674              112.8 ns/op
BenchmarkInterfaceCustomTypePointer-8           218896858              111.9 ns/op

Неплохо, разница не в 2 раза, но она видна. Конечно для профита нужно, чтобы сервис был действительно нагруженным. В противном случае из плюсов остается только синтаксис. Но в этих тестах есть несколько смущающих меня моментов.

1) BenchmarkGenericCustomTypePointer сильно выбивается по времени от остальных собратьев;

2) по факту тут у нас не только код работы стека, но и создание объектов. А что если создание объектов вынести за цикл? Ну и чтобы всё было по взрослому - вообще его не учитывать через StopTimer и StartTimer.

Результаты, не учитывающие время создания объектов
goos: linux
goarch: amd64
pkg: github.com/HoskeOwl/goSimpleStack
cpu: AMD Ryzen 7 4700U with Radeon Graphics         
BenchmarkGenericSimpleType-8                    353000452               71.54 ns/op
BenchmarkGenericCustomType-8                    338031572               73.45 ns/op
BenchmarkGenericCustomTypePointer-8             323049594               73.77 ns/op
BenchmarkInterfaceSimpleType-8                  297199362               78.44 ns/op
BenchmarkInterfaceSimpleCustomType-8            298851950               81.57 ns/op
BenchmarkInterfaceCustomTypePointer-8           291001880               83.04 ns/op
PASS
ok      github.com/HoskeOwl/goSimpleStack       191.971s

“А говорят, что дженерики то не настоящие!” - единственное, что хочется выкрикнуть после такого теста. Как только начинаем убирать части программы, не относящиеся к механизмам дженериков, то разница в производительности стремительно сокращается. Плюс мы видим, что BenchmarkGenericCustomTypePointer пришел в норму, значит проблема не в реализации стека. Но есть еще одна вещь, которая также вносит свою лепту - оптимизация компилятора. Давайте отключим и её, добавив нотацию //go:noinline для каждой функции бенчмарка.

Финальный результат
goos: linux
goarch: amd64
pkg: github.com/HoskeOwl/goSimpleStack
cpu: AMD Ryzen 7 4700U with Radeon Graphics         
BenchmarkGenericSimpleType-8                    323416230               72.84 ns/op
BenchmarkGenericCustomType-8                    325032516               71.51 ns/op
BenchmarkGenericCustomTypePointer-8             320308387               75.60 ns/op
BenchmarkInterfaceSimpleType-8                  290263794               81.65 ns/op
BenchmarkInterfaceSimpleCustomType-8            296999368               81.06 ns/op
BenchmarkInterfaceCustomTypePointer-8           291887139               82.64 ns/op
PASS
ok      github.com/HoskeOwl/goSimpleStack       190.310s

Во всех тестах порядок был одинаков. И что мы получили по итогу? Да, дженерики быстрее, но не на много.

UPD: как правильно заметили в комментариях - всё зависит от нагруженности сервиса. В данном случе вывод под ненагруженный проект, в ашем случае 10% может быть значительным приростом.

Вместо итога: на мой взгляд дженерики - хорошее начинание в golang. Я бы не сказал, что с их выходом стоит бежать и переписывать всю свою старую кодовую базу - профита, по производительности, скорее всего не будет. А вот новый код я бы рекомендовал писать через дженерики. Читабельность возрастёт, а количество ситуаций, в которых удастся выстрелить себе в ногу - уменьшится.

И напоследок для самых пытливых - а что произойдет если мы вместо простой структуры с одним полем будем использовать более сложную? С количеством полей от 10.

Ответ
goos: linux
goarch: amd64
pkg: github.com/HoskeOwl/goSimpleStack
cpu: AMD Ryzen 7 4700U with Radeon Graphics         
BenchmarkGenericSimpleType-8                    261289566               90.09 ns/op
BenchmarkGenericCustomType-8                    99811050               228.6 ns/op
BenchmarkGenericCustomTypePointer-8             291460882               80.94 ns/op
BenchmarkInterfaceSimpleType-8                  163295492              149.5 ns/op
BenchmarkInterfaceSimpleCustomType-8            84240980               285.6 ns/op
BenchmarkInterfaceCustomTypePointer-8           311288221               78.62 ns/op
PASS
ok      github.com/HoskeOwl/goSimpleStack       183.679s

Ожидаемо - большие структуры много времени забирают на копирование, в этом случае - дженерики вас не спасут. А положение исправит передача структур через ссылки, причём в обоих случаях.

Комментарии (28)


  1. qrKot
    22.01.2023 19:43
    +9

    И что мы получили по итогу? Да, дженерики быстрее, но не на много.

    Мне кажется, вы немного зажрались (простите за мой французский, не оскорбления ради, а красного словца для).

    В среднем 10-процентный прирост производительности + улучшение читабельности кода... Обычно для повышения производительности чем-то жертвовать принято, например, читабельностью. Да и заради повышения надёжности/читабельности кода как таковой вполне себе куски проектов переписывают, иногда даже на снижение производительности идут...

    А тут "всего 10% в среднем" (это, так то, достаточно много) + читаемость + надёжность кода "и пусть никто не уйдет обиженным"... Ну грех такое ругать, чесслово


    1. HoskeOwl Автор
      22.01.2023 22:51
      -1

      Вопрос как обычно в нагруженности сервиса.
      Давайте представим, что у нас есть небольшой домашний проект. Скажем на 100-150 человек (начальная аудитория). Код в процессе активного переписывания, допиливания и внедрения фич.
      И вот если тут на одну чашу весов положить производительность, а на другю читаемость, то в этом случае я выберу читабельность.
      Моя статья была с позиции домашенго пет-проекта на небольшое количество пользователей.
      Когда мы приходим в мир enterprise, то там совершенно другой расклад. И иногда читабельностью жертвуют даже ради 2-3%, поскольку от общего потребления получается достаточно большая величина.
      Мой вывод не панацея (это надо поправить в статье) и направлен он на небольшие советы. А для более серьёзных проектов - приведены цифры, и по ним будет возможно уже иметь картину для принятия решения на конкретном проекте.


  1. JekaMas
    22.01.2023 20:08
    +1

    А чего автор статьи хотел бы от generics?


    1. HoskeOwl Автор
      22.01.2023 23:24

      Я только в начале пути golang, поэтому мне сложно сейчас будет сформулировать свои ожидания. На текущий момент исследую возможности языка. Но как только за плечами будет больше проектов, то мне действительно будет интересно обдумать ответ на данный вопрос. (Просто еще мало шишек набил на golang, чтобы понимать какие еще боли можно решить с помощью дженериков, а для каких лучше придумать другой механизм)


      1. JekaMas
        23.01.2023 06:33

        Тогда как какие-то выводы возможны? Не лучше ли вместо не очень ясных бенчмарков попробовать найти случаи, где generics нужны в golang? Попробуйте посмотреть на рефакторинг пакета crypto.


        1. HoskeOwl Автор
          23.01.2023 12:53

          Не могу уловить взаимосвязь, раскройте суть вашей претензии подробнее, пожалуйста.


  1. AlexdeBur
    22.01.2023 22:09
    +1

    С дженериками не хватает одной важной фичи (и мне непонятно, почему ее сразу не сделали) - их нельзя использовать в методах структур.

    С учетом, что у меня обычно большинство функций в бизнес-коде - это как раз методы, то дженерики обычно удается применить только прям в очень библиотечных функциях.


    1. gandjustas
      22.01.2023 22:21

      Подскажите, а стандартную библиотеку переписали на генерики?


      1. makarychev_13
        22.01.2023 23:34

        Пока нет, но вроде бы обещали.


      1. JekaMas
        23.01.2023 08:42
        +1

        Да, начали несколько месяцев назад. Хороший пример - пакет crypto https://cs.opensource.google/go/go/+/master:src/crypto/ecdh/nist.go;l=16;drc=d88d91e32e1440307369d50ba17ce622399a8bc1



  1. NightShad0w
    22.01.2023 22:18
    +5

    Дженерики в Го внезапно подкладывают мину совершенно в другом месте, которое не заметно на лабораторных примерах.
    Многие проекты на Го полагаются на рефлексию и глубокую инспекцию типов в рантайме: ORM, сериализаторы, преобразования форматов и типов, тэги структур и все такое. Код на интерфейсах успешно обрабатывается, так как вся информация о типе содержится в рефлексии. А вот дженерики, то ли еще не доделаны до конца (мой опыт с Го 1.19), то ли так и задуманы, но рефлексия на дженерик типах не работает от слова совсем.

    Реальный пример, на котором сам подорвался: GORM библиотека, не то чтобы эзотерическая, и достаточно популярная. Внутри, для укладывания типов в базу данных использует рефлексию. В интерфейсе - interface{}.
    Мой случай - есть 5 разных типов, которые должны лежать в базе данных, дженерик функция с передачей дженерик типа в GORM API, разорвала библиотеку в клочья, когда рефлексия пошла инспектировать тип, идентифицированный в рантайме через абсолютный путь до инстанцированного типа, включая имя файла и всех директорий по дороге. И этот путь использует одинарную обратную кавычку для разделения элементов пути. Вот код GORM был буквально разорван в рантайме из-за непримитивного имени типа.

    Более сложные случаи, когда в базу хочется положить дженерик структуры или структуру с дженерик полем - не проверял, но предполагаю, что проблемы те же.


    1. HoskeOwl Автор
      22.01.2023 23:20

      Очень интересная тема исследования. В статьях говорят, что дженерики компилируют несколько версий кода с нужными типами (т.е. копипаста, но в бинарном виде). Такое поведение с рефлексией действительно неожиданное.


  1. gandjustas
    22.01.2023 22:20

    Это не стёб?


    1. HoskeOwl Автор
      22.01.2023 23:21

      Нет, а что вас именно смутило в статье?


      1. gandjustas
        23.01.2023 10:16

        Статья максимально однобоко рассматривает генерики.

        Начнем с того, что генерики и их подобие есть во всех современных статически типизированных языках. Даже тех, которые долго сопротивлялись их введению. Значит такая фича языка востребована и полезна, решает реальные проблемы.

        Вы как-то усомнились в реальности решаемых проблем в рамках Go, что через чур наивно, поэтому и подумал что вся статья - стёб.


        Вы рассмотрели только одну область применения генериков - обобщенные коллекции. Они, очевидно, будут обладать всеми преимуществами, которые вы обозначили в статье - проверка типов при компиляции, короче код, большая скорость работы, особенно на примитивных типах.

        Но чего вы не упомянули (специально или по-незнанию), что генерики позволяют писать код по-другому.

        Вместо

        func check(e error) {
            if e != nil {
                panic(e)
            }
        }
        
        f, err := os.Open("/tmp/dat")
        check(err) 

        Теперь можно написать

        func check[T any](T r, err error) {
            if err != nil {
                panic(e)
            }
            return f
        }
        
        f := check(os.Open("/tmp/dat"))

        Более того, можно будет (я так понимаю не в этой версии, но в следующих) собрать типы Option[T] и Result[T] с возможностью их комбинации. Что в итоге приведет к исправлению самой большой проблемы Go - обработке ошибок.


        1. HoskeOwl Автор
          23.01.2023 12:51

          В статье я не сомнивался в решаемости проблем, вопрос был один - насколько дженерики повысят производительность в конкретном случае. Более того в статье так-же сказано, что дженерики позволяют писать более лаконичный и понятный код, о чем и сказано в вашем коментарии, насколько я вижу.


        1. aguart
          23.01.2023 17:00

          Обработка ошибок в Go - это не проблема. Тут ничего не надо исправлять.


  1. sbars
    22.01.2023 23:33

    А Pop() в версии с дженериками можно написать менее криптовано?

    • в обоих путях исполнения не используется res

    • в одном пути не используется exists

    P.S. не наезд, просто вопрос по синтаксису.


    1. HoskeOwl Автор
      23.01.2023 00:05

      В случае с дженериками у нас есть необходимость объявлять переменные возврата только ради значения по умолчанию для переменной типа дженерика. На текущий момент я не нашел как вернуть такое значение без указанного механизма. Т.е. использование происходит в момент, когда стек у нас пустой, в 3й строчке функции (хоть и неявное). Так что на текущий момент я не вижу других способов реализации.


  1. Ztare
    23.01.2023 00:12

    В дженерике шире функциональность, из-за этого получился второй параметр. По хорошему вы или в обоих случаях должны возвращать exists, или оба сводить к проверке на nil. Отсюда и жалобы на оверхед по коду


    1. Ztare
      23.01.2023 01:13

      И оверхед работы через interface{} какраз в перепаковке в ссылочный тип, со всеми вытекающими. Отсюда и кумулятивный оверхед по взаимодействию с памятью и GC
      И это написано чуть ли не в каждой статье про них, только не в вашей.


      1. HoskeOwl Автор
        23.01.2023 01:58

        Моя статья не про устройство дженериков, и не про устройство interface{} с преобразование типов (это две достаточно большие темы, которые тянуть на статью каждая), а только про сравнение производительности. Поэтому и глубоких теоретических материалов в ней нет.


        1. Ztare
          23.01.2023 12:47

          это ключевое отличие, без его описания статья не полная. И от него какраз и возникли просадки по скорости, это именно то что вы измерили в тестах


    1. HoskeOwl Автор
      23.01.2023 02:18

      Разверните свою мысль, пожалуйста, зачем переписывание на exists?

      После получения результата из стека нам в любом случае надо преобразовать значение, чтобы смочь его использовать. Значит убирать его в текущих тестах смысла нет - тогда не понятно, что тестируется.


      1. Ztare
        23.01.2023 12:43

        Вариант без дженерика не предусматривает возможность положить в него реально валидный nil
        Вариант с дженериком это позволяет.
        Вариант с дженериком валиднее - разделена информация об отстутствии элементов в стеке и информация о содержимом элементов


  1. qw1
    23.01.2023 13:19
    +2

    Разница минимальная, потому что на каждый Push есть аллокация динамической памяти.
    А теперь представим, что у нас не Stack, а Vector.
    И тогда реализация через интерфейсы будет требовать аллокацию (мы же не знаем, какой объект придёт), а через дженерики Vector[int] может заранее выделить ну скажем 4кб буфер и дописывать в него значения почти моментально.


  1. MaNaXname
    24.01.2023 12:05

    > Все эти примеры оставляют терпкое послевкусие обмана. Сначала завлекают крупными лозунгами, мол Breaking News, прорыв года, все сюда, ваша жизнь не станет прежней! А открываешь статью и там тебе рассказывают, как сложить 2+2
    Хотите посмотреть более интересную задачу для дженериков? Откройте LINQ или Hibernate. Есть аналоги в го?