Pull to refresh

Comments 90

Что люди только ни делают, лишь бы си не учить.

главное, что люди и менеджеры будут всерьёз считать, что они упростили, у них легче, надёжнее, а на С++ пришлось бы... Не знаю, ассемблер писать?

А на самом деле на С++ этот код был бы понятнее, логичнее и быстрее. И там не было бы хаков даже, просто компилятор там всё же поумнее и не надо бороться с гц

Не надо бороться с гц, а надо писать свой гц, который будет бороться с утечками памяти.

вы видимо не знаете что такое деструктор

Что такое деструктор знаю. А вот в какой момент его вызывать действительно не знаю.

невозможно знать что такое деструктор и не знать когда он вызывается. Благо сейчас легко приобщится, попросите у любой ЛЛМ объяснить как устроено RAII в С++

Я что то заключил. Думал, что речь про free из си. А при чем здесь плюсы, речь же про си шла.

лол что?

Ближайшее по "простоте" к С++ это Rust
Тот же самый ельфийский синтаксис с кучей условностей. Зато гибко.

Го простой как кирпич, но при этом он закрывает огромный стек задач.
И самое главное что не только язык прост и легок (во всех смыслах) так и окружение нормальное, покруче даже питона где на долгой дистанции начинаются сюрпризы.

Асемблерные вставки на Го к слову возможны и не слишком сложны.

Go это практически Object Pascal. Часть синтаксиса точно оттуда. Мне нравится именно Object Pascal, пишу на всём практически (основном на С++). И на паскале можно сделать практически всё что и на С++ вплоть до ассемблера (я так делал к апачу и php расширение для вирт файлов).. всё отлично работало.

да, но Го это все вылизано и причесано под современные реалии

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

Не Object Pascal, а Oberon. И Pascal (не монструозный Object Pascal, а маленький оригинальный Pascal) и Oberon созданы Виртом и многие конструкции в этих языках внешне похожи. Именно потому у вас сложилось это ложное впечатление.

я считаю именно так как написал, а что именно вы считаете - ваше субъективное мнение.

Ваш комментарий свидетельствует только о том, что вы незнакомы ни с языком Oberon, ни даже с учебником языка Go авторства Донована и Кернигана.

А на самом деле на С++ этот код был бы понятнее, логичнее

Только в глазах разработчиков на C++. Я бы ещё понял, если бы в пример rust привели, там код действительно читабельный и логичный, хоть и со своими нюансами. Плюсы - это просто сто способов отстрелить себе ногу

Нужна экспертиза по Си и грамотный разработчик в команде, не всегда (редко когда) есть желание нанимать новых людей и плодить зоопарк. К тому же на GO побольше кадровый выбор.

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

да, но очень много до сих пор на си и только на си
те же МК можно на расте но поддержка старого или что-то новое что не хочешь писать "с нуля" то только си

Эмбеддед на расте зависит от зависимостей :)

Не у всех крейтов есть no-std режимы, к сожалению.

Но хуже чем на Си не будет, это да.

Это к слову огромный минус для раста

Пытался как то вкатится. Хоть синтаксис и отталкивал лично меня, но это все еще лучше чем си для долгих дистанций и можно реально на МК компилить, в отличии от Го с его реализацией что пилят полтора агронома и не сильно активно

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

Для серверного и десктопа все в итоге перетянул на Го как раз по причине, что оно работает везде и работает ожидаемо, без сюрпризов

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

А кросскомпиляция в Расте крайне простая, я не видел там особых проблем.

ну в том же го можно завернуть свой сборшик без проблем так что проблема не в куче

чисто с го для ембеддер проблема в том что го заточен под асинхррон, а микроконтроллеры даже под RTOS нифига не асинхронные

С растом же для embedded оттолкнуло что линукс или виндовс - очень сильно влияет с какой машины собираешь для МК и для каждого случая свои подводные. Нельзя как в Го прилинковать сишные файлы или конкретные DLL и оно будет при сборке их юзать на любой платформе где запускается

UFO landed and left these words here

macOS приложения более вероятно будут большей частью на Swift. В отношении ядра Swift вряд ли, Objective-C несмотня на то, что заявлен как один из языков написания ядра, в тех исходниках что публично доступны по факту отсутствует. А вот Си и Си++ как раз присутствуют.

А то что ядро Windows собрались на Rust к 2030-му перевести полностью Рассинович уже вроде как публично заявил. Сделают, не сделают, к 2030-му или позже — это уже другие вопросы.

Objective C в ядре в любом случае сомнителен из-за нетривиального рантайма для поддержки ООП, тащить это в kernel mode так себе...

На мой взгляд в ядре стрёмновато и на Си++ писать. Вернее так: писать можно, но ряд возможностей использовать или стрёмно или вообще нельзя. Те же исключения например. Или множественное наследование. Да и вообще с многими фишками плюсов надо очень осторожно обращаться. А без всех этих фишек плюсы от просто Си отличаются тем, что проще описывать интерфейсы и расширения одних структур в другие (собственно то, чего как раз нет в Си). Однозначно это лучше чем куча макросов, результат которых компилятор проверяет только после того как они кучу какого-то кода сгенерируют. Печаль как раз в том, что все самые "удобные" возможности использовать нельзя.

Полностью согласен. Но по крайней мере код без виртуальных методов (и, естественно, exceptions/RTTI) скомпилируется по сути как обычный C. В Objective С модель ООП от Smalltalk и любой вызов метода разруливается динамически в рантайме - идёт явный поиск в таблицах самого класса и родителей (которые могут динамически меняться).

"В действительности всё не так как на самом деле" (Антуан де Сент-Экзюпери, "Маленький принц").

По факту код без использованию фишечек Objective-C скомпилируется один-в-один с обычным Си и без всякого ObjC Runtime. Этот момент вроде как закладывался изначально в ObjC.


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

код без использованию фишечек Objective-C скомпилируется один-в-один с обычным Си

Так это будет просто код на Си.

Именно так. В этом как выше уже писал была одна из идей ObjC. Что код на простом Си остаётся кодом на простом Си.

Брайан Кокс вообще изначально препроцессор писал, а потом от него к расширению компилятора собственно Си перешёл.

Страуструп кстати изначально тоже ОО расширение к Си делал. Немного по-другому, потому что он идеи из Simula тянул, а Кокс — таки да, из Smalltalk.

В Google создали Golang в том числе для замены сервисов написанных на Си.

Если адрес (указатель на переменную) будет отдаваться за пределы функции, то компилятор будет делать так, чтобы выделить память под неё из кучи, чтобы не задействовать стек.

func megafunc() uint64 {
  // Создаём переменную на куче
  pMyStruct := NewEmptyMyStruct()
  ...
}

В стеке:

func megafunc() uint64 {
  // Создаём на стеке и инициализируем копированием, а не конструктором
  var localStruct MyStruct
  // Далее инициализируем без выделения памяти
  ...
}

Почему в первом случае переменная инициализируется в хипе, а во втором на стеке? Ведь это должно зависеть от компилятора и того как он делает escape analysis, а не от способа объявления переменной. Во втором примере если указатель на переменную утечет за пределы функции, например, если его положить в глобальную мапу или слайс, то переменная также будет инициализирована в хипе.

В первом случае типовой конструктор отдаст указатель, на который будет выделена память из кучи. А выделяется она в конструкторе, который, совершенно не факт, что будет инлайниться. Поэтому 99%, что это будет именно память из кучи.

Про второй случай в статье оговорено, что указатель не должен утекать

Сейчас текст статьи читается так, что использование pMyStruct := NewEmptyMyStruct() всегда аллоцирует переменную в хипе, а var localStruct MyStruct -- на стеке, но это не так.

Ну как видишь люди лайками все доказали

Того о чем вы пишете нет в спецификации языка https://go.dev/ref/spec, а значит компилятор таких гарантий не дает.

Помимо этого, есть вот такой текст в faq https://go.dev/doc/faq#stack_or_heap:

When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

Как видите, здесь нет ни слова о том как переменная объявлена. Только escape analysis и размер переменной определяет то где она будет создана.

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

А во втором случае - сама переменная. Иногда Го оптимизирует и выделяет на стеке. А иногда нет.

В версиях Go 1.24 и 1.25, когда мы подсказываем компилятору как правильно сделать, с большей вероятностью он сделает правильно.

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

func NewFfModInt(v uint64) *FfMod { 
       z := FfMod{v} 
       return &z 
}

func (r *FfMod) Inverse(x *FfMod) *FfMod {
  r = NewFfModInt(0) // происходит аллокация
......
 }

Всё не так однозначно.

NewFfModInt в одном месте может быть заинлайнен компилятором, а в другом нет. Инлайн случится, если компилятор увидит, что указатель не «сбегает» и структуру можно разместить в стеке, и что этот инлайнинг не приведёт к существенному росту числа инструкций в вызывающей функции (не ваш случай).

Я этот пример привёл потому, что исследовал его. Именно его. Аллокация происходит.

По логике её можно не делать. Указатель r и так аллоцирован. Можно его перезаписать. Но компилятор аллоцирует.

Маленькая заметка на полях: __rdtsc(); — это не совсем "такты процессора". Это Time Stamp Counter (TSC) - то есть таймер времени. Грубо говоря (если про Windows говорить) есть три таймера - один миллисекундный таймер по умолчанию работает на 64 герца (в смысле обновляется каждые 15,625 мс), его получают через GetTickCount(), его разрешение можно поднять до килогерца через timeBeginPeriod(), затем есть таймер высокого разрешения, его инкременты можно считать через QueryPerformanceCounter(), и частоту формально надо запрашивать через QueryPerformanceFrequency() , но там обычно 10 МГц. и вот RDTSC, частоту которого можно либо программно получить, тупо отсчитав секунду через тот же QueryPerformanceCounter, либо через CPUID, но только если поддерживается leaf 0x16. Это самое высокое разрешение, обычно сравнимое с частотой процессора, но не равное текущей частоте ядра. Скажем у i7-13850HX P ядра могут легко молотить на 5 ГГц, а частота RDTSC "всего лишь" 2304 МГц, то есть вдвое ниже. Поэтому если для какого-то участка профилируемого кода мы получили два с половиной миллиарда инкрементов RDTSC, то это не означает, что процессор отчканил именно столько циклов, скорее всего их больше пяти миллиардов, просто работал он чуть больше секунды. Конечно, для сравнения скорости выполнения участков кода RDTSC использовать можно и нужно, это самое высокое разрешение, какое только есть, а вот именно количество тактов ядра на данном временном отрезке получить чуть сложнее — тут надо либо RDPMC юзать (и там есть свои сложности), либо что-то типа Intel PCM.

Да, я в курсе, этот хардверный регистр в основном называют циклами, просто я хотел сказать, что с ним надо аккуратно работать, примерно как с QueryPerformanceCounter() - то есть вначале определить его частоту любым удобным способом и дальше использовать. Ну вот смотрите, я нагружу первое ядро процессора на 100% и запущу пару утилит Intel PCM (pcm.exe и pcm-core.exe), они по умолчанию ровно раз в секунду отсчёты делают:

Time elapsed: 1002 ms

 Core (SKT) | UTIL | IPC  | CFREQ |  

   0    0     1.00   2.80    4.36    
   1    0     0.00   1.07    4.07 
...
 Instructions retired: 19 G ; Active cycles: 10 G ; Time (TSC): 2311 Mticks;
 
Core | IPC | Instructions  |  Cycles  | RefCycles  
   0   2.80          12 G     4361 M      2303 M         
   1   0.28         208 K      757 K       422 K     
...

Тут ядро работает сейчас на 4,36 ГГц и реальных циклов процессора набегает в секунду 4361 M (и, кстати, 12 миллиардов инструкций), а вот тиков таймера только 2311 Mticks.

Я не говорю, что в статье неправильно, просто для понимания. Для меня такты процессора - это реальные такты ядра, а TSC - это циклы таймера (который один на все ядра).

Формально если два разных процессора вернули для какого-то участка кода примерно одинаковое количество тиков этого таймера, то мы не можем формально утверждать, что их производительность одинакова без определения частоты этого таймера. Равно как и наоборот - если один и тот же код выполняется разное число тактов TSC на разных процессорах, то это ещё не означает, что один быстрее другого, может у них просто таймеры на разной частоте работают, только и всего.

Само собой, что если мы работаем с одним процессором, то меньшее значение тиков говорит о большей производительности.

А не может ли быть такое, что 4.3ГГц - это частота конвеера CPU. А тактов как раз в 2 раза меньше? ТОгда всё верно. Хотя я слышал, что есть команды, которые до 7 тактов конвеера выполняются.

Нет нет, это именно частота ядра. "Частота конвейера" (если о таковой вообще можно говорить) на примере выше - "двенадцать гигагерц". То есть процессор за одну секунду смог обработать двенадцать миллиардов инструкций, поэтому там параметр IPC - это Instructions per Cycle (инструкций на цикл) - 2.80. Это значит, что в среднем за эту секунду он перерабатывал почти три команды на каждый такт. Есть команды, которые требуют больше одного такта, скажем, умножению надо три. Соответственно IPC может быть и меньше единицы. Вон второе ядро там отработало 757 К тактов, но всего 208 К команд, примерно три-четыре такта на команду.

Из-за спекулятивной и частично параллельной природы вычислений на конкретном ядре некоторые инструкции обрабатываются параллельно. ЛОгично предположить, что частота CPU - это и есть частота конвеера. Маркетологи врядди бы такое упустили.

Да не, там вроде всё достаточно честно и детерминированно, я вот тут на прошлой неделе упражнялся и бенчмаркал ядра, посмотрите, там поподробнее на эту тему, примерно треть можно промотать до места где про латентность и пропускную способность - https://habr.com/ru/articles/957702/

Только по историческим причинам он всегда инкрементировался с фиксированной частотой.

Меня так же мучали заставив писать производительный конвертер графики на java - в спешке переносил сишный код так как то что в java (imageio) полное убожество. Возможность использования нормальных библиотек через jni или сервис отринули - только java, только стенкой в лоб! Менеджеры, им продавать нужно а не делать, типа на java в веб контейнере везде работает а нативный код надо поддерживать на платформах. Получилось так себе по ресурсам, гавнямба... Но работало в 5 раз лучше чем встроенная библиотека.

Понимаю что выбор языка не всегда за программистом.

Почему не С или Rust, если не хочется с управлением памяти возиться а нужна скорость? Go как то ближе с более расслабленным задачам верхнего уровня, там же сборщик мусора.

Я ускорял python (не golang) в 3 раза, в 14 раз, в 40 раз на реальных задачах, а нужно ускорить код в десятки тысяч раз, лучше в пару сотен тысяч раз. И есть некоторые попытки ускорения кода в десятки тысяч раз, только это уже будет немного не python. То есть моё ускорение в десяток раз это полное разочарование.

С языками программирования сейчас общая беда в скорости - процессоры добавили инструкций, железо добавило показателей в производительности, а производительность языков остаётся почти прежней на железе следующего поколение (добавит десяток процентов). Это большая проблема, предпочитают её обходить разными нашлёпками - программными и железом.

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

Лет 20 назад ткнулся в FPGA и попал на CAM (content addressable memory) - реклама заливала что вот щас будет море памяти CAM, в которой поиск в несортированном массиве за 1 клик и это перевернет все, много задач где сейчас требуется сортировка (базы особенно) ускорятся. Помню, гибридные акселераторы для баз данных были (сегменты обычной памяти и CAM). Что то все затихло и пропало.

Где современные транспьютеры, глубокая параллелизация работы с данными? CAM это же по сути и есть апофеоз параллелизации - память на вычислительных ячейках которая сама себя "ищет". Дорого, наверное, не выходит каменный цветок.

беда тут в сравнении интерпретатора с компилятором

Заменил цепочку If на return - таймауты упали в 10 раз в хайлоаде💀

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

Голословно, хорошо бы привести примеры со сравнением скорости.

Кстати, а если также хорошо оптимизировать код на си, насколько он окажется быстрее вашего оптимизированного кода на го?

Я оптимизировал функцию инверсии и достиг 97% производительности от C++ версии. Об этом есть в конце статьи.

По сути, если избежать аллокаций, то Го такой же компилируемый язык как и Си или Си++.

А можно сравнение в виде кода?
Разница в том, что на С++ вы просто написали код и не парились, а про оптимизацию на Go целая статья

Я не писал код на С++. Я взял мегаоптимизированную версии от Jean Luk Pons. И сравнивал свои результаты с ней.

Вот об том и надо было написать в статье. Но только сравнению одной единственной операции ничего не говорит о скорости языка.

И кстати, С и С++ это не одно то же.

Я знаю, что не одно и тоже. Но у них скорости условно одинаковые.
Инверсия в конечном поле - это не просто "операция", это довольно тяжёлый алгоритм.

У меня как у стороннего наблюдателя есть сомнение, что даже обычный код без выделения памяти компилятор Go оптимизирует так же, как какой-нибудь C++ это делает. Я когда с Go разбирался, с удивлением обнаружил, что этот компилятор имеет лишь один режим компиляции, который одновременно и debug и release. Логично было бы предположить, что наличие возможности отлаживать скомпилированный код негативно сказывается на производительности.

Полностью убрать отладочную информацию
go build -ldflags="-w -s" main.go

Только без символов отладки
go build -ldflags="-s" main.go

Только без DWARF информации
go build -ldflags="-w" main.go

Выставление флагов компоновщика конечно может убрать отладочную информацию, но оно не оптимизирует код, который компилятор не оптимизировал из необходимости поддержки отладки. Точно я не скажу, что там конкретно компилятор Go не оптимизируют, го из опыта какого-нибудь C++ я могу сказать, что необходимость отладки сильно ограничивает возможности оптимизации - выкидывание промежуточных переменных, встраивание функций, упрощение ветвлений и т. д.

А по сравнению собственно кодогенератора Go (стандартной реализации от гугла) и clang/gcc есть какие-нибудь сравнения? Как там в плане базовых оптимизаций (анроллинг циклов и т.п.), автовекторизации и прочего? Что насчёт выбора последовательности инструкций под конкретную модель процессора (clang/gcc для этого используют описание микроархитектуры в файликах вроде этих)?

Из gccgo можно выжать векторизацию, но она такая смешная получается, с векторизованной проверкой границ, как я понял. Cерии сложение + несколько сравнений - это же оно?
https://godbolt.org/z/6fPxf3bT6
Как отключить - не в курсе, -B тут не работает.
А в стандартном компиляторе сложных оптимизаций вроде как нет (хотя я не специалист, может чего-то не знаю).

Да, обвязочка вокруг FMA там очень весёлая ) Не уверен, что при этом что-то ускоряется.

Просто переаллокации памяти, о которых в основном речь в статье, на мой взгляд скорееlow hanging fruits. Последние наносекунды - это когда основное время в нашем счётном коде, алгоритмические оптимизации применили, очевидные однородные вычисления векторизовали, с локальностью памяти поработали и надо смотреть на использование ресурсов процессора - раскладка по портам и т.д..

Просто для всех крайности разные)

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

Удивительное дело, но логический тип bool может работать медленнее, чем тип int

потому что современные процессоры работают с 32/64 битами за раз. bool обычно представляется одним битом и для того, чтобы его сравнить нужно как минимум позвать какой-нибудь popcnt и только потом cmp, в то время как с int можно сразу звать cmp.

func (z *uint256) Lsh4() {
	// Мультиприсваивание для сдвига влево на 4 бита
	z[0], z[1], z[2], z[3] = z[0]<<4, (z[1]<<4)|(z[0]>>60), (z[2]<<4)|(z[1]>>60), (z[3]<<4)|(z[2]>>60)
}

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

В отличие от примера GtRaw(x uint256), где у вас просто куча бранчей схлопывается (то есть становится branchless), тут не должно быть никаких проблем с оптимизацией любого из вариантов. Но подозреваю, что настолько мелкие оптимизации приведут к очень смешным улучшениям производительности, что оно не стоит времени, потраченных на написание оптимизации. Было бы куда интереснее, если бы вы такой трюк с нетривиальными типами померяли. Вероятннее всего разница была бы не слишком заметна.

Если в очень горячих местах кода, то улучшения заметные. А эта функция сравнения она часто используется для приведения по модулю.

Я об этом статье не написал, но очень сильный выигрыш дало прямое логическое условие. В горячем IF. Буквально процентов 20.

Было
if !булева_функция

Стало
if булева_функция

Звучит как не очень прямые руки у компиляторописателей. Интересно что у них в ASM генерится. Да и живой бенчмарк было бы неплохо иметь.

Спасибо за статью!

Я тут мимокрокодил (сам пишу на фортране, про другие языки знаю только, что они вроде бы существуют ;-), но статья читается на одном дыхании! А главное, мне теперь стало понятнее, в чем причина эффективности древнего (но осовремененного) фортрана. Ручаться головой за все написанное ниже не стану, но все же в ассемблерный код своих программ я довольно часто заглядываю, так что доля правды в моих "впечатлениях" от этого кода, наверное, есть. А именно, возникает такое подозрение, что большинство из описанных Вами оптимизаций мой фортран-компилятор от Интела делает автоматом (оптимизация на макс.быстродействие). А прикол в том, что мне не нужно для этого танцевать с бубном по исходному коду. Так как язык достаточно примитивный, настоятельно подталкивает клиентов писать попроще, и поэтому у нас, фортранистов, гораздо реже возникает противоречие между читаемостью кода и риском выстрелить в ногу своему быстродействию.

Разумеется, на фортране тоже можно "уйти в глубину", и даже какие-то антипаттерны применять в борьбе за проценты. Только вот это не практикуется, потому что почти никогда не нужно. Например, вместо циклов у нас обычно пишутся массивные операторы. Дальше обо всем заботится компилятор, и, благодаря очевидности логики таких операций, заботится имхо очень неплохо. Я совсем не уверен, что "ручное" управление (т.е попытка вручную расписать такой цикл) позволит программисту простому смертному хоть что-нибудь против этого компилятора выиграть. Или взять использование указателей. У нас это редкость, которая применяется лишь при особой необходимости. Так как в изначальном фортране их не было, традиция всюду пихать указатели просто не сформировалась. Да, это накладывает ограничения на стиль, ну так и язык под это заточен. Например, при передаче параметров в подпрограммы фортран по умолчанию просто отдает туда адрес переменной/структуры. Проблема расхода ресурсов на копирование объекта (которую в "правильных" языках часто решают передачей указателя) не возникает вообще. Правда, у нас потом желательно в заголовке функции написать, что вот этот параметр - сугубо входной, и

менять его функции не позволено (ну или наоборот)

Компилируя функцию, компилятор проверяет, что это действительно так. А при компиляции того фрагмента, где она вызывается - без зазрения совести нагло пользуется этим знанием ;-)

И аналогично с инлайнами, чистыми функциями и т.д. Поэтому, если вам нужен именно перемалыватель чисел (а такие фичи, как поддержка сетевых технологий и пр., не особо актуальны), то вместо рекомендаций:

Kelbon22 > (...) Не знаю, ассемблер писать? А на самом деле на С++ этот код был бы понятнее, логичнее и быстрее

user-book21 > Ближайшее по "простоте" к С++ это Rust

granv111 > Мне нравится именно Object Pascal, пишу на всём практически (основном на С++). И на паскале можно сделать практически всё что и на С++ вплоть до ассемблера 

eandr_671 > Не Object Pascal, а Oberon. И Pascal (не монструозный Object Pascal, а маленький оригинальный Pascal) и Oberon созданы Виртом и многие конструкции в этих языках внешне похожи Именно потому у вас сложилось это ложное впечатление.

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

у нас не хуже, чем в С

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

А главное, порог вхождения в фортран, если Вы математику школьную помните, -

минимальный

Особенно по сравнению с С. Там одни только директивы препроцессора разбирать замучаешься, особенно при отладке. А при попытке их написать без достойного опыта, говорят, многие по ошибке бинарного дьявола вызывали, не говоря уже про UB... У нас с этим все-таки намного попроще ;-)

Только операторы ввода-вывода с кучей встроенных "из коробки" способов доступа к файлам и форматных дескрипторов придется подучить... так как у нас они в виде операторов, а не функций запилены. Ну и для интерфейса, если Вам надо покрасивее, придется какую-то библиотеку прикручивать. Так как "в коробке" у Вас будет только обертка к Windows API.

Ну и последнее неочевидное, но веселое замечание ;-)
А именно, если освоить освоить фортран, то для Вас откроется неожиданная возможность

побыть высокоранговым гуру на досуге

Так как в любой компании, где интересуются разными восточными учениями, или мирами Средиземья и пр., знание древних магических заклинаний, которые при это еще и работают, автоматически повышает статус участника до уровня, близкого к небожителям
;-)))))

Интересное замечание. Есть что-то типа каналов в современном Фортране, чтобы писать многопоточный код? Вообще многопоточность гибкая или нужно на каждый поток одинаковый блок выделять, а потом с геморроем собирать конечный результат?

Спасибо за статью, но вот код "как на Си" вызывает очень большие сомнения.

  1. Размещение переменной на стеке. Вариант того, что переменная утечет в кучу, насчитывал в Escape анализотре - 22 случая. В т.ч., если переменная - параметр вызываемого метода интерфейса (о как). Как Вы гарантируете, что переменная не утекает в кучу, даже простой int (совет не пользоваться интерфейсами в корпоративной разработке не работает, с контрактом может работать больше одной команды)?

  2. Инлайнинг и "маленькая функция". Тоже очень интересует, т.к. Компилятор не инлайнит функции (да хоть и из одного оператора!), если в ней есть defer. То есть, гарантировать что ваша мелко-функция будет заинлайнена - никто не может по факту.

  3. Скорость С/С++ и дорогой вызов функции. Как Вы заставляете компилятор ГО убирать из подготовки вызова встренные запросы проверок на увеличение стека (там не маленькая функция вызывается)? Вызо в ГО - это очень дорого, в сравнении с С/С++

  4. Слайсы (они же строки как иммутабельный слайс). Ну ок. Накатать что-то очень простое, работающее с примитивами типа "байт", "массив" (не слайс!) можно относительно эффективно (если в одну процедуру без вызовов, всё в main) .. но как часто такое нужно? Как Вы избегаете проверок границ слайса в циклах, особенно когда применить range нельзя (например 2 слайса в одном цикле, хоть и одной длины, тупо копирование с обработкой)?

  5. Мапы. Итератор по мапе МАТЕРИАЛИЗУЕТСЯ, в т.ч. и в памяти. Это не просто "перебор" ключей мапы, а именно генерация с рандомом.. А часто, расход памяти на большую мапу дает прирост скорости т.к. доступ О(1) - практически константен.
    ..ну и ещё куча мелких тупостей от компилятора..

Ни разу, сколько не пытался, догнать стек С/С++ на ГО не получалось на реальных задачах. В лучшем случае 1:10. Асинхронщина в ГО.. молчу молчу, сколько раз и куда таскается указатель в криво реализованных каналах. Сошлюсь только на пробегавшее сравнение где-то тут давно уже, что синхронизация на мьютексах шустрее примерно в 6(шесть) раз канальной передачи.
Очень интересно будет почитать про практические примеры оптимизации до уровня С/С++.. может чего не понимаю, в ГО всего лишь 5лет примерно..

Спасибо за комментарий. Речь в статье шла о вычислительно сложной задаче, а не о каналах и сетевых технологиях. Конкретно об инверсии.

Чтобы избежать проверок границ использовались слайсы с чётко заданными границами типа [4]byte.

Естественно, никаких интерфейсов и defer - без которых вполне можно обойтись.

НУ и без мапов. Мапы же используют аллокации.

Возможно следует написать продолжение с разъяснениями этих моментов.

Чтобы избежать проверок границ использовались слайсы с чётко заданными границами типа [4]byte.

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

Речь в статье шла о вычислительно сложной задаче, а не о каналах и сетевых технологиях. Конкретно об инверсии.

Для этого есть Фортран. Впрочем, ГО и тут продует и Фортрану и стеку С/С++ "на раз". Да, и ещё: бенчмарки в ГО не показатель скорости работы. Достаточно заглянуть под капот и посмотреть на накладные расходы.

что проверок на выход за границу массива ГО не делает

К сожалению, при дизассемблировании я видел, что проверки делаются. Если обращение идёт по случайному индексу, не в рамках for.

Впрочем, ГО и тут продует и Фортрану и стеку С/С++ "на раз".

Возможно, мне стоит перенести свой оптимизирвоанный код Го на C++. Будет приколько, если я дооптимизировал до того, что переплюну супероптимизированную С++ версию (от JLP).

Да кто это такая версия JLP? Абсолютно невозможно обогнать на Go С++ в любом отношении, если на С++ код написан такой же

Поддержу. Занимаюсь картографией, одно из направлений, замена опен-сорс (С++) решений на ГО (нужна чистота + доп фичи). Проигрыш в одну каску примерно 1:10 и дальше. За счет много поточности, удается его сократить до 1:2, но дальше увы. Фигушки. Ресурс ограничен и в этом плане, ну по крайней мере у меня. Язык прямой как палка, вариантов что-то улучшить в языке нет как таковом. Компилятор туп как пробка (однократно вызываемое не инлайнится!), вызов процедуры, метода, функции - дорого почти также как в PHP. Проверки, материализуемые итераторы, бегство в кучу из-за принятия Эксейп-анализатором "худших" решений без анализа AST дерева.. Молчу про GC, который штатно запускается только через 1.5сек. от старта сервиса.. Можно на вполне нормальном коде упасть с ООМ-киллером раньше (шутка, он конечно запустится по превышению порога, но .. может не успеть, т.к. делает 2-3 прохода, теоретически - не пробовал).

Прелесть ГО только одна - единственно правильное написание кода, поддерживаемое линтерами и форматтерами - что джун, что сеньор одну и ту же задачу сделав алгоритмически похоже, выдадут один и тот же код.

Второе неоспоримое достоинство - автогенерация кода. В силу особенностей языка - она вполне приемлема в реализациях. Терпимо, в отличии от стека С/С++.. в котором выстрелить себе в ногу можно тьмой разных способов, даже не подозревая о таком. Кстати, утечки памяти в ГО таки есть.. :)

Прелесть ГО только одна - единственно правильное написание кода, поддерживаемое линтерами и форматтерами - что джун, что сеньор одну и ту же задачу сделав алгоритмически похоже, выдадут один и тот же код.

Когда я немножко пробовал Go, принялся для него свою функциональную библиотеку писать, насколько это вообще возможно сделать на Go (можно было взять уже существующие библиотеки, но я писал сам, чтобы потренироваться писать на Go). И после этого всё остальное писал уже обильно обмазавшись монадами и всякими функторами. Так что код быстро перестал походить на традиционный Go Style.

На микробенче с хешмапами

#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> m;
    for (int i = 0; i < 10000000; i++) m[std::to_string(i)] = i;
    int res = 0;
    for (const auto& it : m) res += it.second;
    return res;
}

против

package main                                                                                                                                                      
import (
    "os"
    "strconv"
)

func main() {
    m := make(map[string]int)
    for i := 0; i < 10000000; i++ {
        m[strconv.Itoa(i)] = i
    }
    res := 0
    for _, val := range m {
        res += val
    }

    os.Exit(res)
}

у меня Go немного быстрее. Понятно, что это скорее тест на рантайм и стандартную библиотеку, чем на кодогенератор.

Безотносительно того что это в main и того что происходит в самом бенчмарке (включая то что реализации мапы бывают разные)
С++ код с какими флагами компилируется?

Просто -O3. Можете у себя попробовать и поиграться с флагами.

Насчёт инлайнов. Если в маленькой функции не используется ничего из модулей, то Го хорошо инлайнит

Грубо говоря, если вы пишете конвертатор png в jpg, то можно вытянуть хорошую скорость из Го, если заморочиться.

Покажите. Имхается это не так совсем. В прошлом (1993-? не помню) мы с коллегами писали код на Си, компилируемый 1 в 1 с помощью Watcom-C compiler в ассемблер машины. Код писанный вручную на Ассемблере и код компилятора совпадал до команды, байт в байт.

Обычно рекомендуется при передаче параметра обращать внимание на его размер. Если размер больше 100 байт, то передавать его как указатель, если меньше — можно передавать по значению.

Тут хочется высказать ряд мыслей.

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

  2. Удивляет, что разные источники называют разные цифры, говоря про мифические не то 100, не то 80 байт, при которых можно передавать данные по значению, а если больше — по указателю. В реальности всё проще: Go использует до 9 процессорных 8-байтных регистров для передачи параметров функции, если они в них помещаются (то есть лимит — 72 байта, и он даётся на все аргументы). Если не помещается — кладёт данные в стек и работает уже через него. Кстати, по этой же причине, при передаче в функцию указателя на структуру (8 байт), он передаётся в неё через регистр, а не через стек. А замеры, которые попадались мне на глаза, говорят, что этот подход (регистры вместо стека), пришедший в 1.19, позволил ускорить этот небольшой участок в программах на 40%. Капля в море, конечно, но из таких капель море и набирается.

  3. И, наконец, надо помнить про большие структуры, которые всегда размещаются в куче — это 128 и 64 КБ (или 64 и 16 КБ при использовании опции smallframes) в зависимости от способа объявления переменной (см. https://github.com/golang/go/blob/master/src/cmd/compile/internal/ir/cfg.go). Редко такие встретишь, но помнить стоит.

Многие программисты в RUVDS удивляются, почему они пишут на go менее производительный код, чем на Python.

А руководство компании не удивляется?

Sign up to leave a comment.

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds