Pull to refresh

Comments 226

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

Если вникнуть — есть острый недостаток в SSA формах и их проекциях (array SSA, mem SSA) для решения задач Data-Computational Locality Projection. Для этого приходится часть логики работы с mSSA/aSSA/tSSA выносить на фронт LLVM'a, и плодить mir/mlir'ы, как это было с тем же Rust'ом, и как теперь будет с СLang'ом ...


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

Не могли вы рассказать подробнее о проблемах типизации в ЯП?

Если очень поверхностно, и если не вникать в логику (Constrainted Bunch/Separation logic и прочие расширения логики Хоара если точно), то получается что интерпретацию программ в автоматах Тьюринга надо рассматривать как "многоленточный" автомат — с командами переменной длинны и данными переменной длинны, когда длинна данных зависит от результата выполнения предыдущих команд, а длинна команд от позиций соседних лент.


Так в языке


  1. Не должно быть макросов и прочей кодогенерации, т.к. вся информация о зависимых типах должна быть доступна на этапе компиляции. Вся рефлексия должна быть выполнима на этапе компиляции. Generic'и в топку...
  2. Не должно быть линковки в привычном её понимании — JIT/AOT должен уметь пересобрать с PGO любой отпрофилированный код и выбрать наилучшую реализацию по существующему профилю.
  3. Язык должен быть гомоиконным и строго типизированным (типизированные Lisp'ы 98го года и Shift, например), желательно что бы легко приводился к каноническим минимальным mSSA/aSSA формам без дополнительных трансформаций и свёртки.
  4. Во время компиляции должны быть доступны диапазоны всех принимаемых переменной значений, например http статус от 200 до 600 и т.п., что бы компилятор мог выбрать конкретную размерность типа в зависимости от архитектуры и использовать соответствующий набор команд. Наличие диапазонов очень упрощает обработку ошибок и освобождение/планировку соответствующих ресурсов (сокеты, файлы и прочее).
  5. Желательно использовать зависимые типы для формальной верификации и доказывать прувером отсутствие побочки на всех диапазонах принимаемых значений.
Я так понимаю вы собираетесь в перспективе написать ЯП? Если это так, то было бы здорово услышать все размышления на эту тему

Скорее сам ЯП уже давно написан и решаются сугубо политические и бумажные вопросы.

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

Интересно. Как вы это формализуете? Условно, какие query вы генерируете к пруверу?


Так-то обычно этим заморачиваются сами программисты, когда пишут сигнатуры функций, в которых нет условного IO, и отсутствие сайд-эффектов следует из well-typedness.


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

Как вы это формализуете?

Используется система типов посложнее чем обычно… зависимые типы хранят в себе ещё и диапазоны, или зависимые функции диапазонов принимаемых значений.


query вы генерируете к пруверу?

Пруверы на решётках в общем… точка в решётке — диапазон значений строгий/не строгий или функция диапазона. Прувер должен доказывать что решётки не перескутся между собой и исследовать соответствующие зависимые функции диапазонов. Потом поверх этого есть ещё пару проходов полиэдральных трансформаций для реализации свёрток типов.


и отсутствие сайд-эффектов следует из well-typedness

Сама система типов реализует referential transparency т.к. есть transactional-SSA проекция с mem-SSA. Само блокировки/ожидания короч может расставить, выбрать где лучше скопировать, а где лучше указатель… и как потом Сompare-and-Swap делать etc. Много разных задач решается.


И я бы посмотрел на полноценные завтипы (а не как в ATS) для императивного языка

О них и речь… и это довольно комплексная проблема.

Используется система типов посложнее чем обычно… зависимые типы хранят в себе ещё и диапазоны, или зависимые функции диапазонов принимаемых значений.

Ну это вполне формализуется в рамках обычного CoC, но как отсюда получается отсутствие эффектов (если мы эффектами одно и то же называем, конечно), мне сходу неочевидно.


О них и речь… и это довольно комплексная проблема.

Да, весьма интересно. Ждём статью (хоть здесь, хоть на условном arxiv).

но как отсюда получается отсутствие эффектов

Все эффекты лифтятся аргументами функции при вызовах… да и сам вызов функции рассматривается как "возникновение события" с timeout'ом и cancelation'ом — т.е. можно "отменить" каскадно при возникновении ошибок когда не совпадает range в runtime'e… под "побочкой" подразумевается гарантированная невозможность возникновения "непредвиденных" эффектов i.e. lifelock'ов / deadlock'ов / race condition'ов и т.д. и верификация именно во время компиляции. Можно представить как rust где не нужны Boxed Types, RefCells / Rc, Mutex'ы и ARC вообщe. Так как это решается системой типов.


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


p.s. у меня есть HDL таргеты...

Спасибо за освещение интересной темы! В объясняющих комментариях много нового и непонятного даже с вики.

Например, «гомоиконичность — текст программы имеет такую же структуру, как её AST», но что именно это значит? И почему LISP моноиконичен, а C#/Python — нет?

Не нагуглилось, что такое mSSA/aSSA, прувер на решетках. А «referential transparency т.к. есть transactional-SSA проекция с mem-SSA» — вообще выглядит заклинанием.

Может, найдете время написать статью для людей, которые не в теме компиляторов (но известно, что такое машина Тьюринга)?

И, чтобы два раза не вставать, а как обеспечиваются границы httpStatus от 200 до 600? Тип int(200, 600)? А если сервер вернет 0? Exception при парсинге сообщения?

И что показывают бенчимарки для одинаковой функциональности на вашем языке и на условном C++ / Lisp / C# / Elixir?
текст программы имеет такую же структуру, как её AST, но что именно это значит?

То что интерпретатор лиспа на лиспе будет занимать строчек 20-30.
image


Может, найдете время написать статью для людей, которые не в теме компиляторов (но известно, что такое машина Тьюринга)?

Может быть %)


mSSA/aSSA
Memory SSA, Array SSA.

прувер на решетках

Рассматривается задача приведения термов в многомерном решётчатом пространстве к точке или хотя бы прямой/плоскости. Почитайте что-то про SAT/SMT-пруверы, да и polyhedral.info никто не отменял.


transactional-SSA

Разновидность SSA формы наподобие Rust'овского mIR'a (borrow checks) что бы расставлять блокировки (рассматривается просто STM модель, как в хаскеле и скале с mvar/tvar) при конкурентном доступе.


httpStatus от 200 до 600? Тип int(200, 600)?

Да int(200, 600) :D


Exception при парсинге сообщения?

Да, и автоматическое освобождение ресурсов через bracket-подобные примитивы (см scala cats-effect например, но есть и в котлине через Arrow и в Swift через bow).


И что показывают бенчимарки для одинаковой функциональности на вашем языке и на условном C++ / Lisp / C# / Elixir?

Статическое потребление памяти, задержки и нагрузку на процессор при максимальном трафике сервисов… Бэнчмарки нормальные могу гонять только на Power9/Power10 т.к. там для такого есть вменяемый набор инструкций. В целом от прирост около 30-400% в зависимости от задачи, и потребление памяти гораздо ниже 5-200% тоже в зависимости от задачи.


В основном сейчас язык использую для генерации HDL кода, портирую Power10 ISA.

> Например, «гомоиконичность — текст программы имеет такую же структуру, как её AST», но что именно это значит? И почему LISP моноиконичен, а C#/Python — нет?

Гомоиконичность — внутренняя форма программы совпадает с внешней (там, конечно, влияет перевод текста в атомы-списки-ссылки в машинном представлении, но тривиально и взаимно однозначно).

Когда мы видим на LISP что-то вида (* A (+ B C)), это одновременно и данные, и исполнимое выражение — изначально, без сложного парсинга. Можно его прямо и выполнить, а можно с ним манипулировать стандартными средствами — проанализировать по элементам, создать из частей, переформировать… и тут же и исполнить. Сделали (setq action '(+ a (* b c))), теперь можно сделать (eval action) — и оно выполнилось. Фактически, выполняемый код и данные это одно и то же, представимое одинаково (ну, очевидно, кроме связи с внешним миром, типа встроенных функций).

Естественно, это не бесплатно. Платим за это:
1) Затратами на выполнении — вместо того, чтобы исполнять машинный код, система интерпретирует многоуровневые конструкции. Да, в разных реализациях есть и свёртки во внутреннем представлении в более компактные виды, и JIT кэшированных кусков, но всё это нашлёпки сверху на основную идею.
2) Тем самым знаменитым (засильем (скобочек (во (всех (LISP))))), потому что раз любое выражение (кроме самых примитивных) это список, в котором подсписки и т.д., то и выглядеть они будут все очень похоже.

В случае Python, да и любого другого языка… вот пример. (+ a (* b c)) на Питон будет переведено в a+b*c, тут тривиально. А вот (* a (+ b c)) на Питоне это уже a*(b+c) — видите, пришлось скобки добавить из-за приоритета? Можно было бы и первое писать как a+(b*c), но так обычно не пишут, и уже возникает неоднозначность — ставить их или нет? Далее, пусть мы переводим на C. Было (/ a (deref b)) (где b, например, типа указателя на int), переводим в a/*b… ой, а почему это мы начали комментарий? /* ведь его начинает… значит, вставляем пробелы везде, где хоть малейшее подозрение на возможность неверного парсинга? Или тоже скобки? Сложно, в общем, движения всякие дополнительные.
Некоторые инструкции существовали для удобства программистов на ассемблере (типа rep cmpsw), потребность в них падает.
С другой стороны, «обычные», часто используемые инструкции оптимизируются производителями процессоров так, что можно оставаться на обычных и отказаться от экзотических без потери скорости (типа leave/enter для пролога/эпилога функций)
Тольво вы путаете «можно» и «нужно». Гляньте в табличку. Если LEAVE просто тормозит (3 тика на Ryzen для того, что она делает — это много), то ENTER сегодня — это «гроб с музыкой»: 16 тактов, за это время можно штук 20-30 обычных MOV исполнить!

Исселование, подобное тому, что делает автор разработчики процессоров тоже делают… и в результате на современных процессорах разные экзотические инструкции не то, что бессмысленно использовать — их вредно использовать! Даже руками на ассмеблере! Скорость будет никакая!
может модуль предсказания ветвления не дружит с экзотикой, вот больше их и не используют?
Там не всё так просто. Поскольку компиляторы «экзотику» не используют, то на неё «подзабили»: инструкции есть, но поддерживаются «на отвяжись».

Одно время «забили» даже на MOVS и разработчики упражнялись в подборе оптимального набора SSE инструкций. А в Ivy Bridge «случилось чудо»: внезапно старая добрая MOVSB была оптимизирована и стала работать быстрее любых SSE (для больших объёмов: 256 байт и больше). Интересно — умеет это использовать LLVM или нет…
странно. По Вашей ссылке p11-55^
For copy length that are smaller than a few hundred bytes, REP MOVSB approach is slower than using 128-bit SIMD technique described in Section 11.16.3.1
А моё замечание в скобочках вы проигнорировали?
наверно, я просто неправильно понял «few hundred bytes»- мне казалось, что «несколько сотен»- это больше, чем три сотни (256), а для больших объемов- вроде и так понятно, что раз оно не ложится в один запрос к памяти- то как ни оптимизируй эту инструкцию, скорость ее все равно упрется в ПСП, которой все равно, как оно там в процессоре реализовано, хоть через SSE, хоть через IA-32.
На числодробилке я не вижу особой разницы между movdqa, movapd, movupd и movsd, когда надо больше, чем «few hundred bytes» прожевать: все выдают ~12GB/s.
а я помню, что movsb быстро работала уже в sandy bridge
Она там работала гораздо быстрее, чем у AMD, но медленнее, чем SSE-мувы.
Нет, не в курсе. Но это не так важно: процессоры, которые такое умеют должны выставлять специальный CPUID бит.

Так что бенчмарки при запуске программы устраивать не нужно. Либо Ryzen так умеет и выставляет бит, либо не умеет и тогда нужно использовать SSE.
Либо Ryzen так умеет и выставляет бит, либо не умеет и тогда нужно использовать SSE.

… либо это описано в ERRATA

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

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

Обычно так и собирается под конкретный проц при деплое на железные сервера или в облако, т.к. проц уже заранее известен. Это для десктопных приложений, или проприетарных бинарников с закрытым кодом генерится общий код, а-ля Generic x86-64 с SSE2.

Обычно так и собирается под конкретный проц при деплое на железные сервера или в облако, т.к. проц уже заранее известен.
В облаке проц далеко не всегда известен. Именно поэтому godbolt.org не рекомендует использовать -march=native, например.
А как обвязка зависимостями сторонними зависимостями на этот подход влияет? Одно дело скомпилить один пакет, а другое дело скомпилить все древо заисимостей, что вообще не всегда возможно, а когда возможно то может занять пару/тройку десятков часов чистого времени.

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

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

Было бы интересно сравнить подходы с:

«A Superscalar Out-of-Order x86 Soft Processor for FPGA» Henry Wong, Ph.D. Thesis, University of Toronto, 2017

We have shown in this thesis the design of an out-of-order soft processor
that achieves double the singlethreaded (wall-clock) performance of
a performance-tuned Nios II/f (2.2x on SPECint2000) at a cost of 6.5 times the area of
the same processor. This area is about 1.5% of the largest Altera (Stratix 10) FPGA.

We presented a methodology for simulating and verifying the microarchitecture of
our processor, which we used to design a microarchitecture that is sufficiently complete and correct
to boot most unmodified 32-bit x86 operating systems. We showed that the FPGA substrate differences
from custom CMOS do affect processor microarchitecture design choices, such as our use of a physical
register file organization, low-associativity caches and TLBs, and a relatively large TLB. The
resulting microarchitecture did not require major microarchitectural compromises to fit an FPGA
substrate, and remains a fairly conventional design. As a result, the per-clock performance of our
microarchitecture compares favourably to commercial x86 processors of similar design. Our two-issue
design has slightly higher perclock performance than the three-issue out-of-order Pentium Pro (1995)
and slightly less than the newer two-issue out-of-order Atom Silvermont (2013).

UFO landed and left these words here
Тогда придется перекомпилировать код под каждый процессор. Вы не забывайте, что в процессорах тоже бывают ошибки.
Потому и стали так популярны виртуальные среды исполнения, т.к. на лету могут давать более оптимальный код для конкретного современного процессора, по сравнению с заранее компилированным универсальным вариантом.
Могут… но не дают. C++ всё ещё быстрее — даже без использования экзотики.

Другое дело, что когда выбором заведуют маркетологи реальные цифры никого не волнуют, главное — красивые лозунги.
Интеловский компилятор может делать бинарь, который загрузившись, смотрит, какие расширения поддерживаются процессором, и загружает наиболее оптимальный вариант.
Ага. А перед этим он смотрит, чтобы процессор был от Intel, не AMD, да.
Ну да, есть такое, расширенные инструкции у AMD он не использует.

Даже те, которые полностью совместимы с Intel?

+- да
типичный пример — LinPack: если на AMD-ом проце запустить стандартную версию, собранную Intel-ским компилятором, то результаты в разы ниже аналогичных Intel-процессоров и версии, собранной не Intel-компилятором.
Никакие. Проверка на производителя процессора идёт в самом начале.

И её можно вырезать из бинарника, кстати — есть умельцы. Тогла скорость работы на AMD'шных процах резко возрастает.

В этом нет ничего страшного. Говорю как гентушник дома и компиляющий вычислительный код с -march=native на работе.

Значит, оставшиеся инструкции не настолько ускоряют/уменьшают код, чтобы инвестиции в их использование окупились.

Возможно, интеловский или иной коммерческий компилятор использует больше инструкций.
Любопытно. Следующий вопрос — насколько полно скомпилированные программы используют имеющиеся ресурсы — кэш процессора, ОЗУ, ядра (не говоря уже о GPU).
Любой хром стремится полностью занять ОЗУ, это факт.

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

Смотря что понимать под «реально могут использовать»: интринсики считаются? А стандартные библиотеки?

Если машинный код хоть когда-нибудь появляется в бинарнике, значит реально могут использовать (даже если по факту таких кодов в дикой природе в 0% случаев есть). Если никогда не появляется, о чем установлено обзором сорцов, например, значит не могут.

Тогда покрыто >90% инструкций однозначно: когда в процессорах появляются новые инструкции в Clang/GCC/MSVC/ICC симметрично же появляются новые интринзики для их вызова.

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

Правда, это очень сложная задача. И либо нужно разбирать исходники компилятора, чтобы понять какие инструкции он будет вставлять, либо под каждую инструкцию писать такой C код, при котором данную инструкцию стоило бы использовать.
скорее не «не могут», а «не хотят». Это может быть что-то древнее как какашки мамонта. Которое никто давно не использует, потому что есть способы решить это быстрее и производители процессоров обработку этих команд не улучшают — так как видят, что они никому не нужны. Глупо делать новый процессор на 50% быстрее в командах, которые никому не нужны и пользователи не заметят разницы. Замкнутый круг.

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

у меня нет идей, кстати, как такое исследование провести. Только если не взять какой-нибудь очень большой репо, а не просто /usr/bin И то не факт, что этого будет достаточно. Как минимум, надо ещё кажду популярную операционку проверить — что там у них внутри.

Например, смотреть изменения по поколениями процессоров. Грубо говоря, MMX команды появились в 95-м (от балды), в бинарниках они появились в 2000 (ещё больше от балды) и одновременно пропали FPU команды. Потом пропали MMX, зато появились… Вывод: FPU и MMX не хотят уже

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

исследовать такой код тяжело, проще спросить разработчиков (если они конечно согласятся что-то прояснить).
У меня такое чувство, что хотя процессоры имеют сотни инструкций, компиляторы больше половины из них никогда не генерируют просто потому, что не умеют.
Чувство, очевидно, неверное, потому что, к примеру, VIA Padlock ни одним процессором не поддерживается и даже в ассемблере поддержи нету… но разработчиков OpenSSL это, конечно, не остановило: byte 0xf3,0x0f,0xa7,0xc8 можно написать всегда.

Иногда просто не могут.


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


Короче, как компилятору условных плюсов знать, есть ли у меня нулевые байты в моей входной строке или нет? Не думаю, что если я напишу


for (auto ch : string1)
  assert(ch);
for (auto ch : string2)
  assert(ch);

то он поймет, что я имею в виду.

А как вообще компилятор может понять, какие входные данные будут поданы Вами на вход программы? никак.
Ну компиляторы предоставляют всякие атрибуты и built in для подсказок. Ну, а, вообще, это скорее проблема языка, так как часто мы знаем какие значения может принимать байт или группа байт, но ни C, ни C++ возможностей указать это не предоставляют.
На уровне языка такая возможность есть:
void foo(int bar) {
    if (bar < 0 || bar > 42) *(int*)0;
    ...
--означает «мамой клянусь, bar будет от 0 до 42».

Что с такими указаниями делает компилятор — это другой вопрос.
В ряде случаев clang умеет ими пользоваться: godbolt.org/z/AbQk-p
Ну это хак, а не фича языка. Да и хотелось бы, чтобы в C++ была возможность типам диапазоны значений указывать.

Вот есть у нас Utf-8 нуль-термированная строка. Первый тип — октет. Октет принимает такие-то значения. Второй тип — «символ». Это динамическая структура от 1 до 4 октет. Первый октет вот такой-то и по нему можно узнать длинну этой структуры, второй октет может принимать вот такие-то значение и т.д. Третий тип — сама строка. Есть один символ, означающий конец строки, он встречается всего один раз и всегда в конце. При чём он состоит из одного октета, который полностью заполнен нулями. Если я неправильные значения запишу в данные типы, то согласен на UB.

Ну в завтипах можете это выразить:


data Utf8Char = 
   OneByte (a : Byte ** a `LTE` 0x7F)
   TwoByte ((a, b: Byte) ** (a `GE` 0x7f && a `LTE` 0xC0 && b `LTE` 0x7F)) 
   ThreeByte ((a, b, c: Byte) ** (a `GE` 0xC0 && a `LTE` 0xE0 && b `LTE` 0x7F && c `LTE` 0x7F)) 
   FourByte ((a, b, c, d: Byte) ** (a `GE` 0xE0 && b `LTE` 0x7F && c `LTE` 0x7F && d `LTE` 0x7F)) 

Правда, не готов сказать, насколько это будет удобно.


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

ааа, ну так std:string- это же, емнип, по стандарту и есть nullterminated string, так что это-то Ваш компилятор как раз знает :-).
Неправда: std::string — это произвольный массив байт, и он может содержать любые символы, в т.ч. '\0'
формально- да. но стд считает, что работать Вы с этим «произвольным массивом» будете именно как с нуль-строкой.

cout << my_string;

выводит только до первого нуля.

// string::begin/end
#include #include int main ()
{
std::string str ("Test string\0 2222");
for ( std::string::iterator it=str.begin(); it!=str.end(); ++it)
std::cout << *it;
std::cout << '\n';

return 0;
}


бегает до первого нуля.

typedef basic_string string;
template < class charT,
class traits = char_traits, // basic_string::traits_type
class Alloc = allocator // basic_string::allocator_type
> class basic_string;
template struct char_traits;
template <> struct char_traits;

а у последнего есть мембер
length //Get length of null-terminated string ( public static member function )

Я понимаю, что технически можно в std::string сложить все, что угодно, но задумана-то она была именно для хранения строк с нулем в конце. А вопрос ведь был о том, знает компилятор- что строка null-terminated, или нет, чтобы использовать оптимизированную инструкцию pcmpistrm из SSE4.2? Так вот std::string ему прямо об этом и говорит. Если нет- то есть и другие контейнеры.
Это потому, что вы используете конструктор из const char*.

std::cout << std::string("Test string\0 2222", 18); напечатает всю строку вместе с нулём, и итерироваться она будет вместе с нулём, и length() вернёт 18.
Мне что-то вспомнилась дискуссия про нуль-терминированные строки и паскалевские строки, начинающиеся с 8 бит длины строки, в результате которой родился мерзкий гибрид, у которого в начале октет длины. а в конце \0. Вроде этот способ является основным в Обероне.
А что в этом плохого? std::string так и хранит — длину отдельно, а в конце терминирующий nul, чтобы и c_str(), и length() работали за O(1).
Отдельно — это нормально, мне не нравится, что в гибриде в первом элементе массива вместо char uint8_t. Ну и тут или длина, или терминатор, всё-таки c_str и std::string разные вещи, не совсем понимаю стремление плюсов к обатной совместимости с чистым С. В 98м это ещё смысл имело.
И сейчас, к сожалению, имеет. Ибо у C++ тупо нет некоторых вещей, которые реализуются через C API — либо C++версии банально менее популярны, либо их вообще нету…

Причём, внезапно, очень много всего, связанного со строками: GetText или LibXML2 какие-нибудь…
Как в 98м имела смысл совместимость с чистым Си, так в 2020 имеет смысл совместимость с С++98.
Это как в басне про шатл и лошадиную задницу.
:-) именно! если явно указать длину строки, то компилятор знает, что лучше при сравнении использовать pcmpestrm, а если не указать- то компилятор знает, что лучше использовать pcmpistrm. Потому что стд предполагает, что string- null-terminated, хотя и позволяет Вам использовать ее и по другому. Вопрос ведь изначально был о том, откуда компилятору знать, будет в строке нулевой символ, или нет? и на это я и обратил внимание- что компилятор не знает, какие именно данные Вы загоните на вход, но вот что он знает- так это какие данные Вы ожидаете на входе, так как эту информацию можно получить из типов данных. std::string предполагает нуль-терминатед, но не гарантирует.
И еще- это не я использую конструктор, это он у меня в std::string такой, его за меня сделали. :-)
> Потому что стд предполагает, что string- null-terminated, хотя и позволяет Вам использовать ее и по другому.

Читаю стандарт (ну, last draft, как обычно)…

The class template basic_string describes objects that can store a sequence consisting of a varying number of arbitrary char-like objects with the first element of the sequence at position zero. Such a sequence is also called a “string” if the type of the char-like objects that it holds is clear from context.


«Arbitrary» не предполагает запрет на элемент с кодом 0.

И конструкторы вида

basic_string(const charT* s, size_type n, const Allocator& a = Allocator());

никак не ограничивают сделать хоть все символы NULами.

Все операции точно так же могут складывать/искать/итд. с NUL внутри.

Всё, что есть для совместимости — это что s.c_str()[s.size()] должно быть CharT(NUL), и возможность получать в аргументах const CharT* без длины (для перехода с C-style).

> если явно указать длину строки, то компилятор знает, что лучше при сравнении использовать pcmpestrm, а если не указать- то компилятор знает, что лучше использовать pcmpistrm.

Это вполне возможно, но на std::string не имеет смысла — там длина хранится всегда.
если явно указать длину строки, то компилятор знает, что лучше при сравнении использовать pcmpestrm, а если не указать- то компилятор знает, что лучше использовать pcmpistrm.

pcmpistrm на моих машинах всегда эффективнее, чем pcmpestrm. Соответственно, использовать его можно, когда внутри строки нет нулей.


Вы ожидаете на входе, так как эту информацию можно получить из типов данных. std::string предполагает нуль-терминатед, но не гарантирует.

Начиная с кажется C++11 гарантирует.


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

Было бы интересно сравнить clang и icc. По идее, icc как раз должен использовать все, что можно, чтобы ускорить код, т.к. по идее построен с учетом знаний о внутренней архитектуре интеловских процессоров.

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

Надо ещё смотреть профиль использования приложения. Если взять на userland-приложение, а драйвер уровня ядра или само ядро, статистика немного поедет.
а теперь отрезать неиспользованное и получить RISC посмотреть сколько площади освободиться

Думаю что мало, меньше 10% — большая часть редких инструкций просто микрокодом в RISC транслируется же.

Ну так x86-64 имеют RISC ядро. То что много инструкций поддерживаются процессором, не значит, что каждая инструкция распаяна, много делается программно. Просто сейчас процессоры скорее некий сервер, которому мы говорим некие абстрактные комманды, а он уже делает что хочет. Вон, в Intel ME встроили. По сути другой процессор, который ещё ОС Minix исполняет. Сейчас процессоры — системы на чипе.
Заметил (давно), что i387 инструкции больше не используются. Это учтено?

Хотя их и было немного, но могли попасть в дебаг версии.

Ну и было бы неплохо добавить в статистику число тиков на инструкцию как ИТОГО… Архитектуры то разные.
Ну например, fstp в clang-10 встречается 1693 раза, fld — 1235 раз.
Я бы это не назвал «больше не используются».
А в 64-бит версиях?

Я встречал мнения, что 32бит покинуты (abandoned), т.е не развиваются.
Вы правы: в версиях для x86_64 инструкций i387 с самого начала было очень мало, а после версии clang-3.5 (2014) их осталось меньше десятка на весь бинарник.
1. Большое количество MOVs и LOADs — это несовершенство аллокатора регистров и генерация кода для Phi-функций SSA представления программы.
2. Компилятор не использует все возможные инструкции, потому что чтобы их применить нужно обнаружить соотвествующие операции в исходной программе. Внутреннее представление (IR) программы в компиляторе (пример llvm.org/docs/LangRef.html) в основном состоит из простых операций, которые чаще всего один в один отображаются в target ISA. Добавление всевозможных сложный операций в IR усложняет написание платформенно-независимых оптимизаций. А в кодо-генераторе полно других важных проблем, которые нужно решать.
  1. Верно, но это сложно как-либо оценить статистикой, т к даже с идеальным регистр аллокатором мувы все равно будут
  2. В LLVM есть пяток проходов, где он пытается по набору операторов распознать всякие интринсики, например https://godbolt.org/z/yRJubW
вы, мне кажется, слегка перегибаете с «состоит из простых операций». начать с того, что в LIR'е операции различают типы, а в во всех мэйнстримных архитектурах — это разные мнемоники.
Простые с точки зрения разработчика компилятора. А именно, каждая инструкция LIR делает только одно определенное действие, не имеет неявных зависимостей и обладает минимумом побочных эффектов. Инструкция явно предоставляет всю информацию о себе, что позволяет писать оптимизации на основе matching, пример InstrCombine pass. Наличие типов как раз упрощает их.
Если бы в IR были бы такие сложные x86 инструкции как 'LOOP' или 'REP CMPS', то всем оптимизациям приходилось бы каждый раз иметь в виду что инструкция делает больше чем одно действие. Это все затрудняет написание generic оптимизаций. Кстати интринсики в IR — это как раз и есть возможность использовать сложные инструкции. Только вот оптимизации не любят интринсики.
По поводу различных мнемоник, как раз простота IR позволяет автоматизировать процесс мэпинга инструкций IR в машинные инструкции. В LLVM за это отвечает tablegen, который берет описание ISA и генерирует таблицу конечного автомата.
это всё прекрасно, что вы рассказываете, но никаким one to one mapping тут и не пахнет. и к слову «каждая инструкция LIR делает только одно определенное действие» это такое себе, вот вам описание инструкции load, к примеру:

result = load [volatile] , * [, align ][, !nontemporal !][, !invariant.load !][, !invariant.group !][, !nonnull !][, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !align !<align_node>]
А что в вашем понимании 1-to-1? Вы говорите:
но никаким one to one mapping тут и не пахнет.

Привидите пример того, сколько инструкций LIR отображается не один в один.
Все что с! — это метаданные, хинты для оптимизаций и кодогенерации. Они могут быть проигнорированы, либо вообще отброшены. Оптимизация не должна использовать метаданные для передачи информации влияющие на корректность операции.
Если отбросить метаданные то определение:
result = load [volatile], * [, align ]
И семантика простая:
«The location of memory pointed to is loaded. If the value being loaded is of scalar type then the number of bytes read does not exceed the minimum number of bytes needed to hold all bits of the type. For example, loading an i24 reads at most three bytes. When loading a value of a type like i20 with a size that is not an integral number of bytes, the result is undefined if the value was not originally written using a store of the same type.»
Привидите пример того, сколько инструкций LIR отображается не один в один.

так вот я же вам привел load. вы там, кстати, выкинули nontemporal, а это вполне могут быть разные инструкции. то есть, то, что LIR load при кодогенерации на АРМе может распадаться на ldr,ldp,ld1,ld2,ld3,ld4 вас не убеждает? вы по-прежнему считаете, что это one to one mapping?
В теории может, но на практике я этого в LLVM не видел.

что LIR load при кодогенерации на АРМе может распадаться на ldr,ldp,ld1,ld2,ld3,ld4 вас не убеждает


Такие вещи могут делать в LLVM backend'е, где оперируют MIR (https://llvm.org/docs/MIRLangRef.html).
Вначале MIR стараются получить как можно близко похожим на LIR. И он неоптимален. Затем этот MIR прогоняют через кучу оптимизаций, где могут делать свёртки/разбивки инструкций (strength reduction/peephole optimizations). Затем MIR трансформируют в MachineCode, который также прогоняют через оптимизации. И эти оптимизации пишутся под конкретный target ISA, где уже оперируют в терминах инструкций ISA.
Цепочка преобразований: LIR-MIR(здесь очень похожи на LIR)->MachineCode(здесь уже все дальше от IR)->Assembly
LLVM позволяет быстро создать кодогенератор с помощью TD файлов, где описывается mappping MIR в Target ISA. Так как этот кодогенератор сгенерированный, то он просто мепит одни инструкции на другие без особого анализа и обработки. Поэтому можно утверждать что исходный IR отображается практически один в один в target ISA.
Если в вашу ISA так просто IR не отобразить, то тогда нужно будет писать такое отображение ручками, где каждая инструкция MIR как-то сложно преобразуется.
LLVM разрабатывался таким образом, чтобы IR максимально легко было отображать на target ISA.
Я помню как мы добавляли ARMv8.x расширения к LLVM. На первом этапе — это просто создание td файлов описаний. Затем мы реализовывали специфичные оптимизации, и то если в этом есть необходимость.
Как-то странно смотрится большее количество инструкций в x86 против ARM в контексте CISC vs RISC. Либо функционально бинарники не эквивалентны?
CISC по определению имеет больше инструкций чем RISC (reduced instruction set computer)
Ээээ… Больше разных инструкций в ISA => больше функционала на одну инструкцию => меньше количество инструкций в коде. Нет?
Даже с моим минимальным опытом, я бы сказал, что весьма неоднозначно.

Только если код пишет человек. Когда код пишет компилятор, гораздо важнее становятся такие вещи, как распределение регистров. Например, для инструкций семейства MOVS на x86 можно использовать только регистры ESI и EDI, что приводит к куче лишних MOV и сводит на нет все преимущества.

1. Есть переименование регистров.
2. Есть shadow registers.
3. AMD64 много чего добавила.

Ну и вообще там всё сложно может быть.
Руками asm код править можно, но не всегда целесообразно.
Но здесь мы говорим о плотности кода. О том, что несмотря на наличие высокоуровневых команд типа movs, для их запуска аргументы нужно разместить в определённых регистрах, а это лишние инструкции. И никакое переименование тут не поможет, потому что оно работает не на этом уровне.
Есть переименование регистров внутри процессора самим процессором.
Ну так mov нам в любом случае писать надо. То, что процессор, вместо физического переписывания данных в регистрах, меняет регистровый файл — его дело. Количество инструкций от этого не меняется.
Intel ушла от этого начиная с i486.
Оказалось выгоднее делать RISC ядро + переводчик команд.
Всё-таки с Pentium Pro, а не с i486. И i486 и даже Pentium исполняют x86 инструкции «напрямую».
С Википедии:
Intel 80486 (также известный как i486, Intel 486 или просто 486-й) — 32-битный скалярный x86-совместимый микропроцессор четвёртого поколения, построенный на гибридном CISC-RISC-ядре и выпущенный фирмой Intel 10 апреля 1989 года.
В английской этого нет. Кто и что понаписал в русской — я не в курсе. μopsы — это P6.
То что вы откопали — это рекламная статья, причём не от Intel, а от кого-то, кто что-то где-то, по слухам, узнал — причём не о 80486, а о будущем P5.

С учётом того, что P5 и P6 разрабатывались одновременно… подозреваю что там «всё смешалось в доме Облонских».

Называть это ну хоть сколько-нибудь надёжной информацией я бы не стал…

P.S. Собственно логика-то простая: разбивать что-то типа inc byte ptr [eax] на три отдельных операции имеет смысл только тогда, когда вы можете выполнить эти инструкции не по очереди, а в каком-то другом порядке. Этого ни 80486й, ни оригинальный Pentium (который P5) не умеют. Спекулятивное исполнение появилось в P6. Который был изначально назван Pentium Pro, а потом, на его основе, сделали Pentium II. И который, несмотря на близость названия, к Pentium и Pentium MMX не имеет никакого отношения.
Я про то, что это не википедисты выдумали: такое действительно писали.
На сайте Падуанского университета тоже висит такое в материалах по курсу Advanced Computer Architectures.

RISC-ядро в 486/P5 чем-то похоже на чайник Рассела: как доказать, что его нет? Особенно при наличии публикаций о том, что оно якобы есть.
Особенно при наличии публикаций о том, что оно якобы есть.
Записать рекламный мусор в «неавторитетные источники» и потребовать ссылки на технический мануал?

В Wikipedia есть механизм, нужно только, чтобы кто-то желал им воспользоваться.

Ну астрологов же из астрономических статей как-то изгоняют?

Иногда такие замечания пытаются «отшить» объясняя, что никаких других мануалов у нас и нету… в случае iAPX 432 это, может быть, даже и оправдано, но когда есть подробные исследован ия микроархитектуры. Тот же Agner Pentium и Pentium Pro подробно исследовал…
downloads.gamedev.net/pdf/gpbb/gpbb12.pdf
Enter the 486 No chip that is a direct, fully compatible descendant of the 8088,286, and 386 could ever be called a RISC chip, but the 486 certainly contains RISC elements, and it’s those elements that are most responsible for making 486 optimization unique. Simple, common instructions are executed in a single cycle by a RISC-like core processor, but other instructions are executed pretty much as they were on the 386, where every instruction takes at least 2 cycles. For example, MOVAL, [Testchar] takes only 1 cycle on the 486, assuming both instruction and data are in the cache-3 cycles faster than the 386”but STOSB takes 5 cycles, 1 cycle slower than on the 386. The floating-point execution unit inside the 486 is also much faster than the 38’7 math coprocessor, largely because, being in the same silicon as the CPU (the 486 has a math coprocessor built in), it is more tightly coupled. The results are sometimes startling: FMUL (floating point multiply) is usually faster on the 486 than IMUL (integer multiply) !

Декодера CISC -> RISC похоже что нет, но работа подобна RISC.
но работа подобна RISC.

Камень подобен сердцу человеческому и в нём заключен кристалл сияющий!(с)
Декодера CISC -> RISC похоже что нет, но работа подобна RISC.
Ну маркетологи и не такое придумают.

В каком оно месте «подобна RISC»? Да — и CISC и RISC слегка размытые понятия, но… вот примерно так:
A RISC computer has a small set of simple and general instructions, rather than a large set of complex and specialized ones. The main distinguishing feature of RISC is that the instruction set is optimized for a highly regular instruction pipeline flow. Another common RISC trait is their load/store architecture, in which memory is accessed through specific instructions rather than as a part of most instructions.
Первые два критерия явно не в кассу: 80386 от 80486 архитектурно отличается на 4 инструкции (BSWAP, CHPXCHG, WBINVD и XADD), всё остально — такое же.

Load/Store тоже нету (это в P6 завезли). Так с какой стороны это RISC? Только со стороны отдела продаж… ну так они и трактор самолётом назовут и глазом не моргнут…

Кстати в русской Wikipedia прямо написано:
В итоге RISC-архитектуры стали называть также архитектурами load/store.
Да, это «в итоге» и можно при желании, написать, что 80486 называли RISC-процессором… но ни о каком «RISC ядро + переводчике команд» речь не идёт ни в 80486, ни в P5.
В те годы RISC была «стильно-модно-молодёжной» технологией, поэтому производители прикручивали эти красивые буковки ко своему товару. Считалось, что RISC — это «простые инструкции за 1 такт», чему i486 соответствует отчасти.
Нет такого определения. CISC характеризуется сложностью самих инструкций, а не набора.

Например, в таком CISC, как PDP-11, одна и та же MOV выполняет:
— загрузку константы в регистр: MOV #123, R1
— загрузку константы по абсолютному адресу в память: MOV #123, $#456
— загрузку константы по относительному адресу в память: MOV #123, 456(R1)
— копирование регистров: MOV R1, R2
— копирование из памяти в память: MOV $#246, 776(R2)
— копирование из памяти на стек: MOV 776(R3), -(SP)
и много других вариантов, вплоть до совершенно безумных типа сохранить значение из регистра в коде данной команды(!): MOV R4, (PC)+ (потом его можно оттуда извлечь, уже зная точный адрес)

То же самое с пачкой других команд, для которых допустимо самое широкое из доступных разнообразие адресаций (BIS, BIC, ADD, SUB, CMP...)

Цена за это — что процессор должен разобрать адресацию и потратить время на отработку каждой адресации вплоть до всяких «косвенных автодекрементных», это вложенные дополнительные опциональные шаги чего-то прочитать, сложить и т.п.

Сравните с ARM, RISC-V — части операций просто нет (из памяти в память, константа в память), остальные делаются разными командами: загрузка константы в регистр это одно, читать по адресу регистр+смещение это другое, то же самое с предекрементом (как для PUSH) это третье, это всё разные команды (хотя часть различия синтаксически записана как разница в записи операнда в памяти).
Команд — больше. Каждая сама по себе — проста и выполняется с минимумом вложенной многошаговой логики, в идеале укладывается только в один шаблон «прочитал — операция — записал». Превращения входного потока команд в микрооперации просты и в идеале вообще 1:1 (реальность портит, но не радикально). Система команд формата PDP-11 тут требовала бы радикальной трансляции. Даже x86, у которого максимум один операнд в памяти (строковые не в счёт), требует тут трансляции.
x86, у которого максимум один операнд в памяти (строковые не в счёт)

В прошлом топике напомнили ещё и про push [addr]
:-P
Ну отдельные подобные исключения, да, есть. Спасибо за подсказку.
Насчёт PDP-11 — а разве это не просто следствие ортогональности системы команд? Я не большой знаток архитектур, но в таком RISC, как MSP430, система команд так же ортогональна и тот же mov делает так же кучу вещей.
Это не «просто» следствие ортогональности системы команд, это следствие того, что ортогональность системы команд это один из принципов CISC. В идеале — любая команда с любыми операндами со сколь угодно сложными адресациями, и PDP-11 здесь не предел — вершиной CISC были VAX и M68000. За что, похоже, и пострадали — Intelʼу хватило ресурсов перевести x86 на внутренний RISC, Мотороле — нет, и в 1994 была последняя модель новой разработки (embedded не в счёт); DEC похерил VAX в пользу Alpha (но они перегнули палку в противоположную сторону).

RISC, наоборот, принципиально уходит от ортогональности, нагружая автора кода (и человека, и компилятор) тем, что он не может произвольно сочетать всё со всем, и вообще сильно меньше может: сложные многоэтапные адресации — в топку, операции память-память — к ногтю, адресации больше чем с двумя регистрами — с корнем, и так далее. Зато исполняется полученное легче и дешевле (не нужно трансляции, проще блоки синхронизации внутри конвейера...)
А куда тогда относится какой-нибудь 6502, где система команд, с одной стороны включает в себя разные конструкции типа «возьми адрес ячейки, там адрес другой ячейки, а там уже данные», а с другой стороны — набор команд столь ограничен и крив, что при написании программ возникает в основном вопрос «а как на этом чуде вообще хоть что-то написать»?
Так много чего можно отнести к совершенно разным классам одновременно.

Вот S/360: с одной стороны, команды типа сложить/умножить бинарное или плавучее — не имеют вариантов с получателем в памяти; они могут читать из памяти, но не писать. Значит, когда можно «A 3,4(12)», но нельзя «A 4(12),3», это больше RISC, чем x86, в котором может быть и «add ebx, [r12+4]», и «add [r12+4], ebx».
Но с другой стороны, в нём есть какие-нибудь AP и EDMK, которые занимаются итерированием десятичной записи в памяти. Эта часть — в RISC такое не вводят, а команда целиком для оптимизации работы программисту — значит, CISC.

6502 многие относят к RISC, за счёт простоты большинства команд и того, что ради экономии тактов там даже сделали некоторые выломы из традиционной логики (например, стек там поставтодекрементный/преавтоинкрементный, в отличие от почти всех остальных). Но я бы его отнёс к кастрированным инвалидам, из-за урезанности которых вообще различие начинает терять смысл, но за счёт вариантов типа «LDA ($36,X)» — его проектировали как урезанный CISC, а не RISC…
Ну и регистров для RISC откровенно мало (нулевая страница — это не регистры, как бы ни хотелось обратного его поклонникам...) в общем, типичный продукт мышления 70-х. Если бы за 6800 не просили в 3 раза больше, про 6502 никто бы и не знал.

> при написании программ возникает в основном вопрос «а как на этом чуде вообще хоть что-то написать»?

Ну я в школе на нём целый Форт наваял… но были вдохновение и новизна, да. Сейчас буду только плеваться на такие идеи.
Кстати, посмотрел я на доки по MSP-430… RISC тут с заметной натяжкой, если не сказать жёстче.

An example: Let’s say you want to clear a word in memory at the address dst. To do this, a MOVE instruction could be used:

MOVE #0, dst

This instruction would have 3 words: the first contains the opcode and addressing mode specifiers. The second word keeps the constant zero, and the third word contains the address of the memory location.

Alternatively, the instruction

MOVE R3, dst

performs the same task, but we need only 2 words to encode it.


Ну это откровенно стиль CISC, как PDP-11. В RISC, во-первых, разделили бы загрузку константы в регистр и запись из регистра в память. Во-вторых, старались бы сделать все команды одной длины, а если константе требуется полная ширина — грузили бы её по частям или из памяти рядом с кодом. ARM, MIPS, SPARC, PPC, RISC-V — у всех тут одни и те же проблемы и сходные решения. В ARM/64 вообще полную 64-битную константу надо грузить в 4 команды (каждая вписывает по 16 бит), 32 бита большинство вписывает в 2 этапа (старшая или младшая вперёд — уже особенности местного стиля).
В-третьих, не было бы такого, что только режим адресации меняет, будет ли читаться константа, смещение к регистру, и т.п., или просто из регистра; да, по сравнению с PDP-11 самые переусложнённые методы вроде косвенного преавтодекрементного — срезали, но само различие — осталось. Даже в ARM чтение из регистра — одно, а из памяти — другое.

Так что, мало команд — да, не перезапутано — так себе, RISC — ой нет ;(
В ARM/64 вообще полную 64-битную константу надо грузить в 4 команды (каждая вписывает по 16 бит)
От компилятора зависит. Clang в 4 делает, MSVC — в одну.

Но да, это уже «читерство», конечно.
Интересно, какой из этих способов быстрее, или же на разных чипах по-разному.
От чипа не сильно должно зависеть, а вот от кэширования памяти — напрямую. Потратить 16 байт кодового потока там, где он напрямую и читается для выполнения, или 8 байт где-то рядом (насколько рядом — зависит от объёма функции и стиля линковки, в общем, вполне может быть соседняя кэш-строка или даже пара килобайт вбок).
Ну и хранение константы где-то рядом приводит к тому, что в кодовых секциях появляются данные — как раз для анализа данной статьи может быть жуткой диверсией :)
Так по поводу MOV #0, dst и MOV R3, dst — в MSP430 есть 2 хитрых регистра, генераторы констант — R2 (который ещё и Status Register) и R3.
Если правильно понял — MOV #0, dst как раз заменится на MOV R3, dst.
А что странного? ARMv8 ISA более выразительная и обычно требуется меньше инструкций для аналогичного кода. Часто код ещё и меньше занимает.
Если не верите, проверьте сами на gcc.godbolt.org
В исходниках LLVM eсть немного "#if defined(__i386__) || defined(__x86_64__)".
Но основное различие в том, поддержку каких targets включили при построении Clang/LLVM. Если все бинарники с llvm.org построены с одними и теми же настройками, то отличия должны быть минимальны.

Что-то мне подсказывает, что все мультимедиа расширения, начиная с MMX в коде компилятора встретить сложно, ибо они не про то вообще. Кстати, было бы интересно проверить эту гипотезу

Не подтверждается: pxor в clang-10 встречается 5933 раза, pcmpeqb — 5193 раза, и т.д.
А выше чем SSE? А, к примеру, AVX'ы, которых по числу инструкций как бы не больше, чем всего остального вместе взятого?
SSE2 используется, AVX нет.

(Нет, инструкций AVX пока ещё не больше, чем всего остального вместе взятого, а примерно 30% от общего числа. Об этом был мой предыдущий пост.)
вы считали AVX только по мнемоникам, я так понимаю? но в 512-м есть, во-первых, маскирование с двумя режимами. есть отсутствие маскирование (вырожденная маска), есть broadcasting bit, который тоже меняет семантику. посмотрите сюда:

software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=AVX_512

это разбивка 512-го на семантически разные — с точки зрения интела — куски. предположим, мы поделим это на три (с учетом разной ширины регистров), всё равно это больше тысячи функционально разных операций.
подождите, а не подтверждается что именно? для pxor и pcmpeqb есть оба варианта. вы смотрели на их аргументы? я сомневаюсь, что современные компиляторы генерят код для mmx — медленно и регаллоку лишняя головная боль.
Не подтверждается именно то, на что я отвечал: «что все мультимедиа расширения, начиная с MMX в коде компилятора встретить сложно»
так мой вопрос именно в этом: инструкция может быть больше, чем в одном варианте и если там xmm регистры, то это уже не MMX, хоть и название то же самое.
Вопрос в том, что встречать расширениями мультимедиа :)

В 64-битке SSE — основной механизм для плавающей точки. Соответственно операции с ней (в самом компиляторе их таки есть) — исполняются на SSE.

SSE активно используется для заливки памяти нулями — pxor + movaps/movdqu/etc. составляют вообще основную часть всех операций. Сюда же копирование памяти.

Это основное, что видно по простому «objdump -d | grep xmm».

Можно сказать, что они используются в компиляторе не по назначению, но если они есть в процессоре, то почему бы и не применить? ;)

А так —
$ objdump -d libclang-cpp.so.10 | grep xmm | wc -l
188295
$ objdump -d libclang-cpp.so.10 | wc -l
7884211


2.4% всех команд это немало.
objdump -d libclang-cpp.so.10

У вас здесь та же неточность, что и у Pepijn de Vos: вы дизассемблируете не только .text, но и неисполнимые данные.
На одном только исполнимом коде он бы 411 разных мнемоник не набрал :-)
Я уверен, что нет. objdump -d по умолчанию разбирает только те секции, которые помечены как исполнимые.
Для компилированного x86 нетипично складывать данные рядом с кодом, поэтому тут ложных срабатываний не должно быть. Я их видел в некоторых библиотеках типа libcrypto, где много ассемблера с ручными фокусами, но не в clang. И ещё я просмотрел результат грепа глазами (по тысяче строк в начале, середине и конце) — если бы там был дизассемблинг данных, начались бы массы команд очень странного содержания и примерно равномерное распределение по регистрам, а этого не было.
Для компилированного x86 нетипично складывать данные рядом с кодом, поэтому тут ложных срабатываний не должно быть.
Некоторые версии некоторых компиляторов пихают таблицы для switch и работу с вариадиками в код.

Но в типичной программе этого добра действительно немного. Так что процент кода вы посчитали верно, а вот разных мнемоник — действительно могло из-за этого насчитаться…
> в типичной программе этого добра действительно немного

Естественно. Поэтому надо чем-то вначале проверить, откуда можно вычитывать данные, а откуда нет — и самые редкие отфильтровать уже вручную.

Я прошёлся после вчерашнего вопроса tyomitch'а по /usr/bin рабочей машины простой проверкой — у кого в выхлопе такого objdump -d будут rcl или rcr? — один таки нашёлся: zoiper. Не знаю, зачем его так собрали, но там таки попадают данные в декодирование, вот характерный пример:

dbeefe: 9b fwait
dbeeff: c1 d2 4a rcl $0x4a,%edx
dbef02: f1 icebp
dbef03: 9e sahf
dbef04: c1 69 9b e4 shrl $0xe4,-0x65(%rcx)
dbef08: e3 25 jrcxz dbef2f
dbef0a: 4f 38 86 47 be ef b5 rex.WRXB cmp %r8b,-0x4a1041b9(%r14)
dbef11: d5 (bad)
dbef12: 8c 8b c6 9d c1 0f mov %cs,0xfc19dc6(%rbx)
dbef18: 65 9c gs pushfq
dbef1a: ac lods %ds:(%rsi),%al


Если бы Pepijn de Vos отбраковал такое перед основным анализом, то цифры явно бы уменьшились.

> Некоторые версии некоторых компиляторов пихают таблицы для switch и работу с вариадиками в код.

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

Но я не знаю — отправили они правки в upstream или нет.
Хм. даже в спектруме было около 1200 инструкций, насколько я помню. Их стало меньше?
Я думал наоборот должно быть гораздо больше
Это пермутации всех регистров.
Самих инструкций гораздо меньше.
Вот в ARMv1 лишь 45 различных инструкций и 23 мнемоники.
en.wikichip.org/wiki/arm/armv1
Но ведь каждая такая инструкция, имеет отдельный машинный код и следовательно выстроенную для нее логику в железе камне процессора?
Машинный код сформирован по простым правилам — есть поля кода операции, регистров и непосредственных данных.
ADD A,r
10000 rrr
|     ^ номер регистра источника
^ код операции ADD a,r
000 b
001 c
010 d
011 e
100 h
101 l
110 (hl)
111 a

LD R,r
01 RRR rrr
|   |  ^ номер регистра источника (см. выше)
|   ^ номер регистра приёмника 
^ код операции LD r,r

01 110 110 является невозможной операцией ld (hl), (hl), поэтому вместо неё другая.
Причём бесполезные LD A,A LD B,B имеются.

или операция BIT n,r

префикс CB 
01 nnn rrr
|   |  ^ номер регистра (см. выше)
|   ^ номер бита
^ код операции BIT


Т.о. мы имеем 135 различных кодов (8+63+64), но по факту это лишь три инструкции.
clrhome.org/table
Например
LD DE, xxxx — 11xxxxh — 00010001 0000000000000000
LD A, xxxxh — 3axxxxh — 00011101 00000000000000000
LD A, xxh — 3exxh — 00011111 000000000

Как данная схема обрабатывается процессором на уровне архитектуры кристалла?
Тут присутствует паттерн для инструкции LD, а затем просто маркер для работы с разными регистрами, либо все таки для всех трех инструкций есть отдельный электрический путь?
p.s. да, тут третья команда выбивается работой с верхней частью регистра, но дана для примера, что IMHO LD могут быть разными инструкциями?
Если вы про Z80, то в первоисточнике первая инструкция называется LXI, вторая LDA, а третья MVI… (если не напутал).

А с другой стороны CMP и SUB в большинстве процессоров — это «почти одно и то же», первая инструкция просто в регистр значение не записывает… Есть даже процессоры, где её просто нету, вместо неё регистр с неизменно-нулевым значением…

PCMPEQB/PCMPEQW/PCMPEQD/PCMPEQQ/PCMPGTB/PCMPGTW/PCMPGTD/PCMPGTQ — это восемь разных инструкций x86 (причём они ещё и в разных процессорах появились: SSE2, SSE4.1, SSE4.2!), а в ARM — это всего-навсего одна инструкция VCMP…
Ваша ссылка ведет просто на статью в википедии, где про инструкции ни слова…

Вычитал вот тут: en.wikipedia.org/wiki/Instruction_set_architecture, что вроде как я был прав, считая инструкциями все варианты, включая перебор регистров.

То есть инструкция — это собственно полная команда, которая включает в себя opcode и возможно допольнительные байты с data

Для инструкций типа LD, если параметром или одним из них является регистр, то для каждой комбинации LD <регистр> будет разный opcode.
А если параметром будет значение или адрес — это уже просто data, которая идет после opcode.

Итого:
LD A, 0000h — одна инструкция.
LD B, 0000h — другая инструкция.
LD B, 0001h — таже вторая инструкция с другими данными.

Таким образом количество инструкций которое поддерживает процессор нужно считать с перечислением всех вариантов с разными регистрами.
Таким образом количество инструкций которое поддерживает процессор нужно считать с перечислением всех вариантов с разными регистрами.
Я не знаю кому и зачем это нужно. Знаю только что для подобного маразма даже маркетолого не додумались. Когда MMX описывается как 57 новых инструкций — то это точно не по вашей методике делается. А иначе они бы в одной им MOVD насчитали бы сотни инструкций — там для задания регистров есть аж два байта: ModR/M и SIB.

А после появляения x86-64 их количество бы, примерно, учетверилось бы.

Для инструкций типа LD, если параметром или одним из них является регистр, то для каждой комбинации LD <регистр> будет разный opcode.
А если будет одинаковый? У VFMADDPD 4й регистр в байте immediate задаётся, а у CMPPD там же задаётся условие.

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

Вполне возможно, что в z80 так и было, но сейчас и регистров больше и наборов регистров больше и микрокод существует, поэтому ситуация вполне могла измениться.
Уж в 8080/z80 — так не было на 200%. Если вы посмотрите на карту опкодов 8080, то обнаружите, что ровно четверть её (64 инструкции из возможных 256 кодов) занимает ровно одна инструкция mov. Зачем же реализовавывать шесть десятков раз почти одно м то же? Да и вообще: как вы это себе представляете? 256 опкодов, 6000 транзисторов, 24 транзистора на опкод… что вы в 24 транзистора упихаете? А учтите, что в эти 6000 транзисторов нужно ещё уложить и регистры и модуль общения с памятью и кучу всего ещё…

Конечно же всё было совсем не так: увидев, что старшие биты опкода 01 — всё «уезжало» в реализацию одной инструкции MOV и все 63 инструкции обрабатывались по одному шаблону. 63 потому что, что «MOV (HL), (HL)» (которая должна была, исходя из логики декодирования, переслать адрес из ячейки памяти в неё же) вызывала у процессора «несварение» и он «замораживался». Ей просто дали название HLT и так и оставили…

А у 6502 было ещё круче: все опкоды пропускались через «таблицу декодирования», где было пара десятков строк (точное число не помню). И они были подобраны так, что на каждый документированный опкод реагировали 3-4 «строки» — и что-то делали.

Вместе — получилась не вполне бессмысленная система команд (хотя и очень-очень странная).

Самое смешное, что незадокументированные опкоды тоже активизировали какие-то строки и тоже что-то делали… ну что им захотелось — то и делали.

Энтузиасты, разумеется, всё происследовали и составили список

Так что нет — никогда и нигде, ни в одном процессоре, реально созданном людьми, ваше странное правило «если разный opcode, то в кристалле процессора должна быть реализована логика для каждой отдельно взятой инструкции» не соблюдалась.

P.S. Странно только что вы об этих самых-самых азах не знаете. В том смысле, что если вам это всё неинтересно… то почему вы вообще об этом пишите? А если интересно — то информации ж вагон (в том числе на Хабре), зачем же выдумывать?
все опкоды пропускались через «таблицу декодирования», где было пара десятков строк (точное число не помню)

По вашей же ссылке написано, что 130 — т.е. на порядок больше, чем «пара десятков».
130 — это количество ячеек в этой таблице, а не количество строк.

А где-то видел статью, где всё это подробно разбиралось.
Ага. Теперь всё понял. Кто-то считает, что это таблица 130x21, кто-то что 21x130.

Так как это всё — не из чтения документации, а из исследования чипа под микроскопом, то сложно сказать кто прав… достаточно чип повернуть на 90 градусов — и строки станут столбцами, а столбцы строками.

На фотографии по вашей ссылке, где эта «таблица» подкрашена зелёненьким, строк ну явно сильно меньше, чем столбцов кстати.
Это понятно, что группы инструкций типа LD не имели индивидуальной обработки, и в архитектуре для них были общие транзисторы до определенного момента. Это логично и правильно.

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

По вашей же ссылке, где указывается, что добавлено 57 новых инструкций, есть конкретный документ, где они описаны:
www.intel.com/content/dam/www/public/us/en/documents/research/1997-vol01-iss-3-intel-technology-journal.pdf

Overall, 57 new MMX instructions were added to theIntel Architecture instruction set

и ниже таблица, подпись к которой гласит, что это ВСЕ mmx Инструкции. И похоже, что 57 как раз означает разные варианты, потому что в таблице едва десяток, с указанием на разные размерности наберется десятка два. И из этого уже нужно насчитать 57.

Заголовок спойлера

Во-первых, с указанием размерностей уже получится 35 штук — это не так сложно подсчитать, как кажется. Во-вторых, если смотреть по полным спискам в мануале, например, кроме PADD{B,W,D} будет PADDQ — и это не более поздний набор фич, это всё тот же MMX. Так что журнальная публикация наверняка просто не всё перечислила. Откуда 57 — не знаю. Текущий мануал говорит про 47 (том 1 глава 9.4). Может, 57 — просто опечатка.

А вот если бы там была зависимость от регистра, то каждую надо было бы умножать на 64 (mm0..mm7, 2-3 регистра), а если учесть варианты адресации памяти — то и на пару тысяч. Я вот уверен, что этот подход там не применялся :)
Во-первых, с указанием размерностей уже получится 35 штук — это не так сложно подсчитать, как кажется.
Вы второй столбец забыли. PADDB и PADDSB это разные инструкции.

Во-вторых, если смотреть по полным спискам в мануале, например, кроме PADD{B,W,D} будет PADDQ — и это не более поздний набор фич, это всё тот же MMX.
А зато вот с насыщением там только два размера: PADDSB и PADDSW.

Текущий мануал говорит про 47 (том 1 глава 9.4).
Он говорит про 47, они там даже приведены… и как раз PADDQ там и нету.

Похоже кто-то решил, что 57 — это опечатка и лишние инструкции «выкинул за ненадобностью». А вот если вернуть «забытые» PADDQ и PSUBQ и засчитать каждый из 8 сдвигов как два (там две версии у каждого из них: одна сдвигает на значение в MMX регистре или памяти, другая на значение, заданное в инструкции), то как раз 57 и получится.
Текущий мануал же явно отличается от того, когда MMX ввели впервые. Я больше обсуждаю то, что касалось z80

www.z80.info/z80arki.htm
The Z80 CPU instructions length can be from one to four bytes long. To increase the Z80 CPU speed most instructions are only one byte long. 252 instructions are one byte, the rest are 2, 3 or 4 bytes long.

Что означает, что посчитали как раз все инструкции, и 3 специальных, которые начинают 2-3-4 байтные инструкции.

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

LD могут быть разными инструкциями?

Разумеется тут все 3 инструкции разные, с одинаковыми мнемониками, для удобства.

Но когда вы говорили про 1200 инструкций, вы считали
LD a,n LD b,n LD c,n LD e,n LD d,n и т.д. как разные инструкции, а она одна.
Я почему-то всегда считал, что
LD — это команда
«LD a,n» это инструкция, и поэтому у нее есть конкретный машинный код напрямую обрабатывается уже архитектурой CPU

Я был неправ?

P.S. Понятно что у современного компа есть уже несколько чипов еще на материнке, добавились микрокоды внутри процессора, и так далее. но в Z80 было проще.

Я вот со времён КР580/Z80 считал, что LD — это мнемоника, LD , — это команда, а LD a, 0 — конкретная инструкция

А разве у КР580 не 8080 ассмеблер? Там такого бардака ещё не было: одна мнемоника = одна инструкция = один шаблон.

А вот уже в Z80/8086 начался «разброд и шатание». Причём вот на самых-самых простейших инструкциях.

Вот такое вот:
a0 34 12 mov al, byte ptr[0x1234]
8a 06 34 12 mov al, byte ptr[0x1234]

Это вот две инструкции или одна? Заметим что на 8086 первая — в полтора раза быстрее второй. А вот уже начиная с 80286 — без разницы.

Он самый. Но времена-то одни. Дома на 580, в школе на Z80. И как-то после привыкания (580 раньше на пару лет дома появился) даже нравиться стало. Но вот в институте на 8086 уже как-то ассемблер перестал радовать. То ли система команд, то ли "640 кб хватит всем" и нет смысла байты экономить, то ли стало нравится решать прикладные задачи.

Я думаю времени перестало хватать. Если просто задуматься: Б3-34 вышел в 1983м году, а Клуб Электронных Игр его исследовал Еггогологию до 1988го. Пять лет.

Вы сегодня можете себе представить, чтобы с одной платформой сегодня возились столько лет и копали так глубоко? Причём не в одиночку, а тысячими, совместно?

Просто «соблазнов» стало больше, уже на ассемблер и машинные коды стало не хватать терпения…
> Вы сегодня можете себе представить, чтобы с одной платформой сегодня возились столько лет и копали так глубоко?

Ну я тоже копал еггоги :)
А что ещё делать, когда это единственная толком доступная электронная игрушка, и интересно, как она работает?
Было бы что-то поприличнее и поконструктивнее (хотя бы с постоянной памятью! МК-52 был дорог и редок) — занимались бы чем-то менее специфическим.

> Причём не в одиночку, а тысячими, совместно?

А сколько человек, по-вашему, занимаются тонкостями работы процессоров x86? Явно ещё больше, причём не только из чисто поржать :)

> уже на ассемблер и машинные коды стало не хватать терпения…

Вот вполне хватает.
А сколько человек, по-вашему, занимаются тонкостями работы процессоров x86? Явно ещё больше, причём не только из чисто поржать :)
Вот только этих процессоров — десятки. Какая нибудь ALTINST со своим «подземельем» (причём более глубоким, чем у МК-52) — только на C3 есть.

Вот вполне хватает.
Да ладно? Даже никто не составил карту незадокументированных инструкций, не вызывающих #UD, по моделям! Это, извините, дюже халтурная Еггогология.

Хватает, максимум, инструкцию почитать…
Справедливости ради, инструкция там хорошо за 4к страниц.
У ARM — 8 тысяч с лишним. Причём из последних версий много чего выкинули, раньше больше было.

Но это потому что, Aarch32 и Aarch64 имеют между собой мало общего и описываются, фактически, отдельно.

Я потому, кстати, и сказал «почитать», а не «прочитать». Не знаю — читал ли все эти тома хоть кто-нибудь «от корки до корки».

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

Подозреваю, что для i386 использовался набор инструкций Pentium Pro, для x86_64 Athlon 64 (SSE2), из-за совместимости. Возможно i386 тоже расширен до SSE2.
В i386/x86_64 есть SSE3/4.1/4.2/AVX инструкции? Ну для интереса MMX?Вижу MMX есть, что весьма забавно.
Возможно i386 тоже расширен до SSE2.

Как минимум, stmxcsr и ldmxcsr в версии для i386 используются начиная с clang-3.3 (2013)

В i386/x86_64 есть SSE3/4.1/4.2/AVX инструкции?

Нет, только до SSE2.
Мне вот как разработчику на FPGA интересно было бы провести некоторые параллели или скорее узнать у осведомлённых, что лучше: одна и та же программа с использованием минимального набора инструкций или наоборот максимального с изменением типа инструкции каждый такт (желательно).
Для параллельных систем понятно, что чем больше распараллеливания, тем проще обработка.
Для процессорных с одной стороны нет разницы, но вот если задуматься… По сути набор одинаковых инструкций задействует одни и те же цепи логических элементов, соответственно тепловыделение данных микрозон будет больше. С другой стороны, если будет задействовано максимальное разнообразие инструкций, с частой сменой логических последовательностей между тактами, нагрев микрозон процессора будет меньше и теплоотвод увеличится (теоретически) и получится некое распределение инструкций. Соответственно меньше температура -> больше частота (меньше троттлинг).
Как-бы исход противостояния RISC и CISC процессоров подсказывает ответ. В целом мало быстрых но простых инструкций лучше чем много сложных но медленных. Если есть избыточная емкость FPGA, можно фигачить одинаковые блоки и перебрасывать исполнение между ними по кругу, чтобы распределить тепловыделение. А оттуда и до суперскалярности недалече.
А при чём тут RISC? Там и архитектура и микроархитектура абсолютно другая. Вопрос задавался об оценке работы на одинаковом процессоре. То есть зависимость именно софтверная.
И я бы не сказал, что небольшое количество быстрых инструкций лучше — i386 и x86-64 до сих пор занимают 100% десктопных PC. Почему? Наверное потому что они лучше. А всякие АРМы удел мобильных систем, потому что там очень нужна экономия заряда (и то половина мобильных ПК остаются на CISC архитектуре с огромным успехом).
Современный x86 обеих битностей уже давно не CISC технически. Это гибрид — все частые инструкции хардверные и однотактовые или даже суперскалярные, куча редких, которые тянутся для совместимости — медленные на микрокоде. На ARM та же история, но к тому же сильно завязанная на уровень ядра.
А вот настоящие CISCи, типа VAX-11 проиграли свою войну давным давно и исчезли. И в основном потому, что CISC хорош для ручного написания кода на ассемблере, а RISC — для кода, сгенеренного компилятором.

PS Я исхожу из того что — RISC — это идея, а не слово, которое ARM вставил к себе в название
На тот момент, когда ARM вставил к себе в название это слово — название вполне соответствовало содержимому.
Нифига подобного. Одна инструкция, которая может записать в память r3, r7 и r10-r12 (да ещё при этом изменить значение указателя) — это RISC? Одна инструкция, которая может сдвинуть R1 на R2 и прибавить к этому R3 — это RISC? Заметьте что в обоих случаях они исполняются на ARM2 за другое время, чем простые инструкции — потому что ALU-то одно и шина только 32бита за такт может пропустить.

ARM чётко понял что именно круто в RISC: инструкции одинаковой длины. И то — со временем Thumb32 от этого отошёл.

Всё остальное — было проигнорировано уже в ARM1 (экспериментальная версия, никогда не продававшаяся).

P.S. Ну и, конечно, ARM также понял что именно не круто в RISC: «рыхлый код». Отказ ARM от классического RISC был как раз не «непониманием», а куда более глубинным пониманием — не только преимуществ RISC, но и недостатоков тоже.
Это, кстати, реально обидно — что STM у ARM появился уже тогда, а код для i386 до сих пор на 12% состоит из push по одному регистру.
Ну в AArch64 они от этого отказались, потому что угрохать существенный процент всего массива из 4 миллиардов инструкций вот именно на эти все «серийные» загрузки и выгрузки — как-то некузяво.

А 12% — это по количеству инструкций или по количеству байт? Большинство PUSH/POP — однобайтовые (двухбайтовые если REX нужен), так что 12% немного удивляют…
Одна инструкция, которая может записать в память r3, r7 и r10-r12 (да ещё при этом изменить значение указателя) — это RISC?

Да, LDM/STM не являются классическими рисковыми инструкциями.
Даже на x86 их нет, зато они есть в других RISC-процессорах — Power.
x86 — довольно фиговый CISC. На 68к был movem и многое другое.

Одна инструкция, которая может сдвинуть R1 на R2 и прибавить к этому R3 — это RISC?

А вот сдвигатель это чистейший RISC подход. Вы рассматриваете это как две операции, но на самом деле сдвиг — это не операция, а модификатор.
Просто один из входов ALU проходит через сдвигатель.
MOV R0, R1, LSL R2; операция MOV, сдвиг — не операция!

Что важно, ALU в ранних ARM-ах выполняют роль AGU.
Поэтому команды работы с памятью это сабсет обычных ALU операций, но имеющих смысл для работы с памятью.
Кроме мощных режимов адресации это позволяет УПРОСТИТЬ железо — чисто RISC качество.
«Сложные», на первый взгляд команды, на самом деле проще в реализации.

Сейчас, в современных ARM процессорах, конечно сдвиг и ALU разбивают на микрооперации чтобы работать на более высоких частотах. Выделенные AGU тоже есть, чтобы не занимать ALU.

И то — со временем Thumb32 от этого отошёл.

У ARMv8 нет VLE, тем не менее плотность кода — выше.

x86 — довольно фиговый CISC
Зато он довольно неплохо «ложится на железо». Если суперскалярность не требуется, конечно.

А вот в современных x86 процессорах… там много чего пришлось устроить, чтобы это как-то засуперскалярить…

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

P.S. В ARMv8, конечно же, VLE есть, я думаю вы AArch64 имели в виду.

P.P.S. А Intel, как обычно, решил очень широко шагнуть… и в результате обделался со своим Itanic'ом «по самое нехочу»…
На ARM та же история, но к тому же сильно завязанная на уровень ядра.
На ARM чуть-чуть другая, хотя и похожая история. Для того, чтобы понять почему x86 выиграл «на рубеже веков» далеко смотреть не нужно — достаточно первых трёх строк таблички.

x86й код — банально плотнее, чем RISC. Это улучшает утилизацию кешей, шины и прочего. А уже внутри-внутри — там да, μopsы и всё вообще хорошо.

ARM код, внезапно, плотнее, чем даже x86. Да, это куплено дорогой ценой (инструкции не следуют «классике RISC» даже в самом первом ARM2, дальше у нас Thumb16, Thumb32 и всё такое) — но результат… в табличке.

А AArch64 — ещё плотнее. Именно ради этого AArch64 имеет очень мало общего с ARM (32-битным). Это совсем другая ISA, очень аккуратно продуманная и спроектированная под максимальную плотность инструкций.

Потому что, внезапно, оказалось — что это важнее всех остального.

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

Вы анализируете бинарники как массив байт. Многие из них мертвы (не важны). Например, если сервис при старте читает конфигурационные файлы, оглядывается вокруг насчёт версии ОС и поддержки фич, настраивает логгинг и т.д., то это может занимать пол-бинаря. После чего запускается tight loop, который работу делает, и там может работать 0.0001% от всего бинаря 99% времени.


Правильнее было бы использовать perfcounter'ы и посмотреть, какие инструкции чаще всего исполняются на современных компьютерах. Есть вероятность, что всякие SSE/AVX и прочие интринзики, которые в бинаре почти не видны, окажутся на довольно высоких позициях. Например, видео на ютубе — какие инструкции исполняются в этот момент?

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

Возможно инструкций используется гораздо больше, например в Gentoo с -march=native. Предкомпилированные дистрибутивы собираются с максимально совместимыми наборами инструкций.

Так большая часть программ в /usb/bin/, в основном, и занимается тем, что копирует данные из одного места в другое.
Анализируя /usb/bin/, вы не увидите системных инструкций. Их надо искать в ядре, в несжатом образе. Если вы сидите не на gentoo, собираемой gcc+graphite или clang+polly с какими-нибудь флагами -march=native -Ofast -ftree-vectorize и т.д. с последним процессором, так там и не будет ничего. Потому что дистрибутивы собирают программы с тем, чтобы они работали на как можно большем количестве процессоров. И лишь в очень небольшом количестве программ есть runtime определение доступных процессорных команд. И скорее всего там они ассемблером закодированы, а не компилятором порождаются. Вообще, мне кажется что надежнее грепом пройтись по исходникам компилятора, а не в бинарниках смотреть.
У Intel есть Clear Linux.
Вроде требовал AVX2. Смотрю сейчас
Instruction Set: 64-bit
Instruction Set Extensions:
Supplemental Streaming SIMD Extension 3 (SSSE3)
Intel® Streaming SIMD Extensions 4.1 (Intel® SSE 4.1)
Intel® Streaming SIMD Extensions 4.2 (Intel® SSE 4.2)
Carry-less Multiplication (PCLMUL)

Работал с AVX2 где-то на 20% быстрее других линуксов. Щас — не особо быстрее.

НЯП, просто собирается с опцией компилятора, разрешающей пользовать нужные команды.

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

«В общем-то известно, что одних инструкций mov достаточно, чтобы написать любую программу» — это крайне занимательно… вот бы компилятор, выдающий рабочий бинарник исклочительно из mov :-)

Вроде как этого не очень сложно добиться.
Mov — полна по тьюрингу, следовательно любую другую инструкцию (и в целом любую вычислимую функцию) можно выразить в mov’ах.


Сходу нашел такой пример:
https://github.com/xoreaxeaxeax/movfuscator

Кто-нибудь разбирался, как он делает циклы, не используя само-модифицирующийся код?
Интересно, хотя я думал там будет более алгоритмически чистое решение, а не системно-зависимые page faults
Это тоже интересный проект.

Но я ожидал получить эквивалентный код.
То есть, если у меня была процедурка на асме, например, вычисляющая НОД двух аргументов, не привязанная к ОС, я бы хотел получить такую же на mov-ах, тоже не привязанную к ОС.
в режиме по умолчанию компилятор не будет, например, использовать SIMD команды новее SSE2, а их там предостаточно. Также он зачастую не будет использовать устаревшие x86 инструкции.
у архитектуры armv7a какая-то особая команда mov, что она выделена красным, хотя у всех остальных синим?
Эти бинарники и не должны использовать многие инструкции, которые поддерживают не все процессоры данной архитектуры. Правильнее (но сложнее) было бы скомпилировать Clang для разных CPU (здесь указано, как получить их список: stackoverflow.com/questions/15036909/clang-how-to-list-supported-target-architectures)

Объясните неумному:
установил для своего ARM64 приложение из apt. У меня есть поддержка NEON, а вот приложение из apt собрано с поддержкой NEON? Наверное нет, а ведь есть ещё куча менее распиаренных расширений (cx16, SSSE3 и др), получается, надо бы всё пересобирать из исходников на целевую машину? Звучит утопично...

получается, надо бы всё пересобирать из исходников на целевую машину? Звучит утопично...

Вы стали на шаг ближе к смене apt на emerge.

SSSE3 — это же x86? Непонятен вопрос в этом смысле.
А так — да, код всех программ в дистрибутике собирается под общую гарантированную базу.
Во времена 32-битного x86 это стало очень критично где-то к концу 1990х (новые возможности стали давать заметное ускорение), и поэтому были варианты, например, скачать дистрибутив в версиях i386 и i586, i486 и i686, и так далее. Первый — для слабых машин или экзотических процессоров (были всякие Via, RISE и т.п.), второй — для свежего мейнстрима.
Ну а когда у меня был парк фрях, я в /etc/make.conf держал установки типа «CFLAGS+= -pipe -march=pentium4 -mtune=k8 -msse2».

На x86-64 с наличием гарантированной базы в виде SSE2, CMOV и т.п. ускорение с новыми наборами уже характерно не для всего, а только для особых задач — а с этим уже легче сделать выбор конкретной реализации алгоритма в рантайме, заготовив несколько адекватных. Поэтому там проблема «пересобрать всё под себя, иначе теряем 20-30%» ушла.

Как с этим в ARM/64 — не знаю, но вроде бы базовые векторные наборы обязаны присутствовать? Тогда тоже большинство задач не получат заметного выигрыша от пересборки («заметного» это, грубо говоря, больше 3%).

Утопичного ничего нет — есть дистрибутивы, предназначенные для локальной сборки, начиная со знаменитого gentoo. Только время придётся потратить — вот приползёт какой-нибудь новый llvm и, если без кросс-компиляции, двое суток непрерывного хруста…
Как с этим в ARM/64 — не знаю, но вроде бы базовые векторные наборы обязаны присутствовать?
Нет, но дистрибутив пересобирать не нужно.

Дело в том, что у ARM масса опциональных фич (включая, скажем, всю плавучку и лаже поддержку 32-бит, не говоря уже о 64-битах). А вот у «профилей Android» или «серверных профилей»… там уже вариаций сильно-сильно меньше.

Так что для телефонов или серверов проблемы нет, а для всего остального… вот зачем вам дистрибутив общего назначения на каком-нибудь Arduino Due? Вы будете Chrome или Firefox запускать на процессоре в 84 MHz и 96KB памяти?

Не, теоретически там это всё можно даже запустить… и даже выяснить — окроется ли там эта страничка Хабра за день или пара недель потребуется… но смысл?
Готовят Fedora Next с требованием AVX2.
Смысл — есть. Особенно — для серверов.
Но Пентиумы и селероны будут в пролёте.
Сделать две версии — обычную X86-64 и продвинутую с AVX2 — достаточно просто.
Кстати, AVX512 пока что медленнее, чем AVX2, из-за перегрева камня и снижения частоты.
Кстати, AVX512 пока что медленнее, чем AVX2, из-за перегрева камня и снижения частоты.
Ну не, это всёж-таки неправда. Там хитрее ситуация.

Как только вы включаете AVX512 — у вас сразу падает частота. Но если вы сможете, при этом, эффективно задействовать «широкие» векторные инструкции в целых 64 байта шириной — то вы можете отыграться и получить даже 60-70% ускорения. А вот если у вас нужно ещё и «узкие» данные обрабатывать, наряду с «широкими»… то может получиться даже замедление.

Так что смысл от AVX512 есть, но… не всегда.
www.phoronix.com/scan.php?page=news_item&px=LLVM-Clang-10-AVX512-Change

www.phoronix.com/scan.php?page=news_item&px=LLVM-Clang-10.0-Features
— For Intel AVX-512 CPUs, -mprefer-vector-width=256 is now the default behavior for limiting the use of 512-bit registers due to the AVX-512 downclocking that can occur. This matches the behavior of GCC now while those wanting the previous behavior can pass -mprefer-vector-width=512 if wanting to increase the use of 512-bit registers but with possible performance implications from the AVX-512 frequency impact.
Only those users with full accounts are able to leave comments. Log in, please.