Как стать автором
Обновить

Комментарии 29

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

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

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

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

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

ну у нас же кейс: И читабельность повысилась, И производительность. Ну т.е. мы не выбираем, чем жертвовать, а просто выбираем оптимальный вариант.

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

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

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

Вы как-то усомнились в реальности решаемых проблем в рамках 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 - обработке ошибок.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории