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

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

НЛО прилетело и опубликовало эту надпись здесь
В докладе выше говорится о том что скоро можно будет давать PDF описание архитектуры и утилита сама сделает ассемблер для нее. Для этого требуется парсить более-менее человеческий язык и всякие таблички из PDF. А тут вот Intel выложила описание, которое с самого начала предназначено для чтения программой.
Забавно, что в итоге конвертация из XED таблиц легла на меня. :)
Первый CL из этой серии уже улетел на go-review.

Возможно в Go 1.11 будет новый x86.csv. И ещё кое-что полезное

А в компиляторы С можно перенести новую архитектуру ассемблера?

Теоретически, думаю, безусловно можно. Таким же способом, как делается (собираются делать) для Go. Но я не слышал о том, что кто-то это делает.
Статья понравилась, спасибо.
Интересно в чем принципиальная разница между GO ASM, .NET CIL, JVM кодом и LLVM кодом, помимо регистровой/стековой направленности?
Дело в удобстве? В том смысле «это сделано не нами», а у нас мы легко и быстро поправим генерацию под любой CPU?
В чем принципиальная разница я не знаю, но мне кажется что вокруг Go компиляции\утилит\рантайма настолько много уже накручено, что просто «переход» или использование чего-то не своего звучит как плохое решение.

Про LLVM тут есть чуть информации.
JVM, NET, LLVM объектные файлы, после генерации кода компилятором, проходят(могут) этап оптимизации, что упрощает разработку компиляторов, в отличии от описанного случая, в котором все CPU подгоняются под одну «гребенку», а компилятор — «один на всех, и за ценной не постоим», очень прозорливое решение, позволяющие не заботится поддержке зоопарка, например тех же x86 с их таймингами, разным количеством исполнительных блоков. Куда интереснее вопрос, был ли в NS32000 SIMD и графический сопроцессор?!
x86 с их таймингами, разным количеством исполнительных блоков
Зачем тайминги в ассемблере, ни разу завязку на тайминги не видел, это же не real time OS и если не параллельное исполнение команд, то какая разница сколько исполнительных блоков…
Есть несколько способов достичь одного результата.
Компилятор должен выбрать, что быстрее — инструкция сдвига влево на N разрядов, или add eax,eax, повторённый N раз, или конструкции типа lea eax, [eax*8].

Причём на разных процессорах в пределах линейки x86 предпочительные инструкции разные, отсюда и проблема зоопарка.
Спасибо.
«Есть несколько способов достичь одного результата.» — мне казалось это количество тактов инструкции.
Тайминг это наверное все же реальное время.
Duduka наверно имел ввиду количество тактов.
Все куда печальнее, выбор инструкций зависит не только от количества тактов (большинство регистровых инструкций выполняются за один такт или меньше, часто — несколько инструкций исполняется одновременно), но от распараллеливаемости потока инструкций (блок исполнения в последних моделях имеет достаточную свободу в последовательности исполнения, а не только спаривание, как в MMX), и раскрутка цикла не всегда приводит к росту производительности (инструкции из предыдущей итерации или инициализации процедуры, цикла могут останавливать исполнение). Так же бутылочным горлышком являются кэши(и необходимость их прогрева, и трюка с предвыборкой в кэш, увеличивающий затраты по тактам, но дающие выигрыш по таймингам на некоторых архитектурах или проигрышу на других), а тайминги выборки из ОЗУ от процессора практически не зависят (вернее сам процессор подстраивается), поэтому на топовых чипсетах с топовой памятью в режимах с минимальными циклами управления, даже слабенький по частоте процессор показывал приемлемый результат на коде интенсивно работающем с ОЗУ, в то время как топовый на «устаревшей» ОЗУ работал как с половинной частотой, недогружая конвеер команд и забивая шину доступом к ОЗУ. Как результат, используется трюк — перевычисление значений, лишь бы не лезть в память можно тратить такты.
Поэтому, выбор только по тактам приемлемых последовательностей инструкций, подставляемых компилятором, после i8086, представляется «неумным», в топовых компиляторах предоставляются ключи выбора типа процессора, и системы SIMD команд, меняющих генерируемый код(оптимизируется под конкретное устройство), в котором (по идее) все последствия учтены, что не факт, например компиляторы MS достаточно долго были заточены на генерацию кода под i386, и на пентиумах исполнялись очень-очень не оптимально, из-за не учета спаривания инструкций и предсказания переходов (и ядра выходили с оптимизацией под i386, но это не факт, что очень плохо, например gentoo-пользователи оптимизировали всю OS тотально, и получали несколько процентов прироста или никакого, сам компилятор накладывал ограничения — падал или генерировал некорректный код). В общем, интел создал систему «интегральных костылей», а компиляторы должны по сложности приближаться к искусственному интеллекту(или не должен,… как выше в топике описан), чтобы один «костыль» не блокировал другой.

Хмм… всё это хорошо, конечно, но такой код вряд ли будет высокопроизводительным. По сути, универсальный ассемблер — это «наибольший общий делитель» всех архитектур. Но каждая платформа имеет свои особенности, а x86/amd64, в частности, большое количество дополнительных инструкций, позволяющих в разы ускорить вычисления. Взять хотя бы эту статью. Как я понял, Go никак не стремится использовать эти расширения, т.к. они не кроссплатформенны.

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


Скажем, если на какой-то платформе отсутствует инструкция умножения или деления (например, на микроконтроллерах), то либо придётся делать для этой платформы хак в виде сложений/сдвигов, либо делать хак в виде одной инструкции умножения для платформ, где оно есть, либо везде использовать сложения/сдвиги. И судя по вот этой статье будет применяться именно последний вариант.

Оптимизации для конкретных архитектур есть. В том числе использование наборов инструкций, которых нет у других. Например, AES написан с помощью соответствующий инструкций под x86. Для других архитектур реализация уже на обычном Go
Есть ещё вопрос производительности: иногда можно вручную написать код, который работает эффективнее, чем результат работы компилятора. Например, существенная часть пакета math/big стандартной библиотеки Go написана на ассемблере, потому что базовые процедуры этой библиотеки гораздо более эффективны, если вы обходите компилятор и реализуете что-то лучше, чем сделал бы он. Иногда ассемблер нужен для работы с новыми и необычными функциями устройств или с возможностями, недоступными для более высокоуровневых языков — например, новыми криптографическими инструкциями современных процессоров.

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

Рантайм компилируется ровно тем же самым компилятором, который и для пользовательского кода. Поэтому тут разницы никакой.

Я говорил вот про этот пакет https://github.com/golang/go/tree/master/src/crypto/aes Как видите, для оптимизации под конкретные архитектуры используется соответствующий постфикс. Похожий подход используется в Go во многих местах. _test.go файлы означают тесты и компилируют отдельно от остального кода. Помимо архитектур в постфиксе можно указать операционную систему, чтобы использовать их уникальные особенности. Например вот так https://github.com/golang/go/tree/master/src/os

Это и есть ручная оптимизация — под каждую платформу код пишется на ассемблере с учётом особенностей этой платформы. Я же говорил про оптимизацию компилятором, как это делает GCC или clang. AES вряд ли соптимизирует, это уж слишком мощное распознавание должно быть, но SSE/AVX применяются автоматически, разработчик же продолжает писать на C или C++, никак не вникая в ассемблер и не подсказывая компилятору. Т.к. эти векторные инструкции (вроде как) недоступны, например, на ARM, там GCC скомпилирует код без них, используя стандартные возможности. Я полагаю, что Go так не умеет, и только об этом и говорил. А использовать интринсики или напрямую ассемблерный код, конечно, можно во многих языках и компиляторах, но это неинтересно в рамках данной темы.

Да, не так понял. Насчет этого уже не знаю точно, но заглянул в компилятор. Судя по этому https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/gen/AMD64Ops.go, поддержка SSE по крайней мере есть. Регистры и инструкции упоминаются. Вот в ARM уже NEON инструкций и регистров не видно.

Спасибо за проделанную работу по переводу. Отличнейший доклад был!

Я сам, увы, не смог присутствовать. Попал в больницу за день до вылета в Денвер. Так обидно.
Кусок на asm ibm360 вызвал ностальгию, на yем лабы в универе делал. Правда это уже было после pdp-11, z80,, masm, tasm и т.д.
Ммм… Например тем что в object file машинный код уже, а статья про более ранний этап «универсального» ассемблера.
Это очевидно.

Но чем некий машинный код (который почти ассемблер) отличается от некоего «универсального» ассемблера (читай ассемблера некой виртуальной машины)…

Вот это мне не очевидно.

И на LLVM чуть ниже сосласлись совершенно справедливо.

В общем пока мне идеология непонятна. Написали потому что «захотелось» такое же, но с перламутровыми пуговицами (на Го).
Отчасти да. Go старается не зависеть от других и в этом его огромный плюс. Это цельный набор инструментов полностью на Go, который берется из одного репозитория и компилируется за считанные секунды. Да, можно было LLVM взять и много чего еще, но все это значит зависеть и поддерживать огромные проекты, которые развиваются самостоятельно, а Go все таки хочется развивать разработчикам самостоятельно и, по сути, в изоляции. LLVM скорее всего дал был Go намного лучший оптимизатор (хотя и комплектный сейчас развивается вполне хорошо), но цена этого была для команды слишком высока. У них даже был C компилятор изначально, но и его они полностью перенесли на Go. Они потеряли в производительности, но цели были совсем другие. Об этом, кстати, тоже был отдельный доклад.
Не совсем понимаю, зачем нужно было городить свои ассемблеры, если уже есть готовое и проверенное решение в виде LLVM IR.
Тем, что им с самого начала не хотелось тащить в проект огромный LLVM. Им нужен был простой тулчей полностью на Go, а LLVM это, как минимум, головная боль со сборкой, модификацией и поддержкой такого огромного проекта. Все таки рантайм Go имеет много своих особенностей, о которых нужно знать компилятору, и просто допилить фронтеэнд и взять все остальное готовое не получилось бы в любом случае. Ну и весь Go собирается за считанные секунды, чего не скажешь об LLVM. Это тоже было очень важно. Теперь, оглядываясь назад на это решение, вряд ли многие программисты на Go скажут, что это было ошибкой. По крайней мере, ни разу не видел даже намеков на какие-то предложения попробовать приладить что-то другое.
Есть реализация Go на LLVM, можно посмотреть.
Два из трех изначальных разработчиков Go имели за собой огромный багаж накопленного. И ассемблеры и компиляторы. И идеи как их развивать.
Возможно Go на LLVM работал бы даже лучше, но мы этого не узнаем…
Как минимум — фатальный недостаток.

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

Это та же причина, почему Microsoft .Net имеет свой runtime, вместо того, чтобы переиспользовать от Java в свое время.
Каких велосипедов только не придумают, лишь бы не использовать LLVM.
Если я правильно понял, то написание эмуляторов для старых архитектур будет проще на всём этом — достаточно оформить красиво документацию к инструкциям?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий