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 и прочие расширения логики Хоара если точно), то получается что интерпретацию программ в автоматах Тьюринга надо рассматривать как "многоленточный" автомат — с командами переменной длинны и данными переменной длинны, когда длинна данных зависит от результата выполнения предыдущих команд, а длинна команд от позиций соседних лент.
Так в языке
- Не должно быть макросов и прочей кодогенерации, т.к. вся информация о зависимых типах должна быть доступна на этапе компиляции. Вся рефлексия должна быть выполнима на этапе компиляции. Generic'и в топку...
- Не должно быть линковки в привычном её понимании — JIT/AOT должен уметь пересобрать с PGO любой отпрофилированный код и выбрать наилучшую реализацию по существующему профилю.
- Язык должен быть гомоиконным и строго типизированным (типизированные Lisp'ы 98го года и Shift, например), желательно что бы легко приводился к каноническим минимальным mSSA/aSSA формам без дополнительных трансформаций и свёртки.
- Во время компиляции должны быть доступны диапазоны всех принимаемых переменной значений, например http статус от 200 до 600 и т.п., что бы компилятор мог выбрать конкретную размерность типа в зависимости от архитектуры и использовать соответствующий набор команд. Наличие диапазонов очень упрощает обработку ошибок и освобождение/планировку соответствующих ресурсов (сокеты, файлы и прочее).
- Желательно использовать зависимые типы для формальной верификации и доказывать прувером отсутствие побочки на всех диапазонах принимаемых значений.
Как вы это формализуете?
Используется система типов посложнее чем обычно… зависимые типы хранят в себе ещё и диапазоны, или зависимые функции диапазонов принимаемых значений.
query вы генерируете к пруверу?
Пруверы на решётках в общем… точка в решётке — диапазон значений строгий/не строгий или функция диапазона. Прувер должен доказывать что решётки не перескутся между собой и исследовать соответствующие зависимые функции диапазонов. Потом поверх этого есть ещё пару проходов полиэдральных трансформаций для реализации свёрток типов.
и отсутствие сайд-эффектов следует из well-typedness
Сама система типов реализует referential transparency т.к. есть transactional-SSA проекция с mem-SSA. Само блокировки/ожидания короч может расставить, выбрать где лучше скопировать, а где лучше указатель… и как потом Сompare-and-Swap делать etc. Много разных задач решается.
И я бы посмотрел на полноценные завтипы (а не как в ATS) для императивного языка
О них и речь… и это довольно комплексная проблема.
но как отсюда получается отсутствие эффектов
Все эффекты лифтятся аргументами функции при вызовах… да и сам вызов функции рассматривается как "возникновение события" с 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.
Может, найдете время написать статью для людей, которые не в теме компиляторов (но известно, что такое машина Тьюринга)?
Может быть %)
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.
Гомоиконичность — внутренняя форма программы совпадает с внешней (там, конечно, влияет перевод текста в атомы-списки-ссылки в машинном представлении, но тривиально и взаимно однозначно).
Когда мы видим на 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… ой, а почему это мы начали комментарий? /* ведь его начинает… значит, вставляем пробелы везде, где хоть малейшее подозрение на возможность неверного парсинга? Или тоже скобки? Сложно, в общем, движения всякие дополнительные.
С другой стороны, «обычные», часто используемые инструкции оптимизируются производителями процессоров так, что можно оставаться на обычных и отказаться от экзотических без потери скорости (типа leave/enter для пролога/эпилога функций)
Исселование, подобное тому, что делает автор разработчики процессоров тоже делают… и в результате на современных процессорах разные экзотические инструкции не то, что бессмысленно использовать — их вредно использовать! Даже руками на ассмеблере! Скорость будет никакая!
Одно время «забили» даже на
MOVS
и разработчики упражнялись в подборе оптимального набора SSE инструкций. А в Ivy Bridge «случилось чудо»: внезапно старая добрая MOVSB
была оптимизирована и стала работать быстрее любых SSE (для больших объёмов: 256 байт и больше). Интересно — умеет это использовать LLVM или нет…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
На числодробилке я не вижу особой разницы между movdqa, movapd, movupd и movsd, когда надо больше, чем «few hundred bytes» прожевать: все выдают ~12GB/s.
Тоже можно понять. Сейчас поколений процессоров накопилось просто охренеть сколько и выбор некоторого "базового" безопасного набора в качестве "швейцарского ножа" вроде как даже и логично. Уверен, если поиграться с флагам компилятора заточив бинарь только под одно конкретное семейство процессоров, можно получить более разнообразный набор инструкций в бинаре.
Обычно так и собирается под конкретный проц при деплое на железные сервера или в облако, т.к. проц уже заранее известен. Это для десктопных приложений, или проприетарных бинарников с закрытым кодом генерится общий код, а-ля 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).
Другое дело, что когда выбором заведуют маркетологи реальные цифры никого не волнуют, главное — красивые лозунги.
Даже те, которые полностью совместимы с Intel?
типичный пример — LinPack: если на AMD-ом проце запустить стандартную версию, собранную Intel-ским компилятором, то результаты в разы ниже аналогичных Intel-процессоров и версии, собранной не Intel-компилятором.
И её можно вырезать из бинарника, кстати — есть умельцы. Тогла скорость работы на AMD'шных процах резко возрастает.
Он иногда не использует и обычные SSE, если проц не от интел.
Значит, оставшиеся инструкции не настолько ускоряют/уменьшают код, чтобы инвестиции в их использование окупились.
Я думал, будет исследование того, сколько инструкций компиляторы реально могут использовать. У меня такое чувство, что хотя процессоры имеют сотни инструкций, компиляторы больше половины из них никогда не генерируют просто потому, что не умеют.
Если машинный код хоть когда-нибудь появляется в бинарнике, значит реально могут использовать (даже если по факту таких кодов в дикой природе в 0% случаев есть). Если никогда не появляется, о чем установлено обзором сорцов, например, значит не могут.
Правда, это очень сложная задача. И либо нужно разбирать исходники компилятора, чтобы понять какие инструкции он будет вставлять, либо под каждую инструкцию писать такой C код, при котором данную инструкцию стоило бы использовать.
Вот я и думал, что будет исследование, чего не могут, а чего не хотят. А оказался банальный подсчет… Что тоже интересно, но не отражает всю картину и это не фундаментальное иследование.
Например, смотреть изменения по поколениями процессоров. Грубо говоря, MMX команды появились в 95-м (от балды), в бинарниках они появились в 2000 (ещё больше от балды) и одновременно пропали FPU команды. Потом пропали MMX, зато появились… Вывод: FPU и MMX не хотят уже
Ну вообще я думал, что нужно исследовать код компилятора и смотреть, какие инструкции он умеет генерировать. Потом попробовать оценить, как часто такие инструкции попадают в конечный бинарник.
У меня такое чувство, что хотя процессоры имеют сотни инструкций, компиляторы больше половины из них никогда не генерируют просто потому, что не умеют.Чувство, очевидно, неверное, потому что, к примеру, VIA Padlock ни одним процессором не поддерживается и даже в ассемблере поддержи нету… но разработчиков OpenSSL это, конечно, не остановило:
byte 0xf3,0x0f,0xa7,0xc8
можно написать всегда.void foo(int bar) {
if (bar < 0 || bar > 42) *(int*)0;
...
--означает «мамой клянусь, bar
будет от 0 до 42».Что с такими указаниями делает компилятор — это другой вопрос.
В ряде случаев clang умеет ими пользоваться: godbolt.org/z/AbQk-p
Вот есть у нас 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
— это произвольный массив байт, и он может содержать любые символы, в т.ч. '\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.std::string
так и хранит — длину отдельно, а в конце терминирующий nul, чтобы и c_str()
, и length()
работали за O(1).И еще- это не я использую конструктор, это он у меня в std::string такой, его за меня сделали. :-)
Читаю стандарт (ну, 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 не имеет смысла — там длина хранится всегда.
Было бы интересно сравнить clang и icc. По идее, icc как раз должен использовать все, что можно, чтобы ускорить код, т.к. по идее построен с учетом знаний о внутренней архитектуре интеловских процессоров.
Думаю что мало, меньше 10% — большая часть редких инструкций просто микрокодом в RISC транслируется же.
Хотя их и было немного, но могли попасть в дебаг версии.
Ну и было бы неплохо добавить в статистику число тиков на инструкцию как ИТОГО… Архитектуры то разные.
2. Компилятор не использует все возможные инструкции, потому что чтобы их применить нужно обнаружить соотвествующие операции в исходной программе. Внутреннее представление (IR) программы в компиляторе (пример llvm.org/docs/LangRef.html) в основном состоит из простых операций, которые чаще всего один в один отображаются в target ISA. Добавление всевозможных сложный операций в IR усложняет написание платформенно-независимых оптимизаций. А в кодо-генераторе полно других важных проблем, которые нужно решать.
- Верно, но это сложно как-либо оценить статистикой, т к даже с идеальным регистр аллокатором мувы все равно будут
- В LLVM есть пяток проходов, где он пытается по набору операторов распознать всякие интринсики, например https://godbolt.org/z/yRJubW
Если бы в IR были бы такие сложные x86 инструкции как 'LOOP' или 'REP CMPS', то всем оптимизациям приходилось бы каждый раз иметь в виду что инструкция делает больше чем одно действие. Это все затрудняет написание generic оптимизаций. Кстати интринсики в IR — это как раз и есть возможность использовать сложные инструкции. Только вот оптимизации не любят интринсики.
По поводу различных мнемоник, как раз простота IR позволяет автоматизировать процесс мэпинга инструкций IR в машинные инструкции. В LLVM за это отвечает tablegen, который берет описание ISA и генерирует таблицу конечного автомата.
result = load [volatile] , * [, align ][, !nontemporal !][, !invariant.load !][, !invariant.group !][, !nonnull !][, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !align !<align_node>]
но никаким 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?
что 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 файлов описаний. Затем мы реализовывали специфичные оптимизации, и то если в этом есть необходимость.
Только если код пишет человек. Когда код пишет компилятор, гораздо важнее становятся такие вещи, как распределение регистров. Например, для инструкций семейства MOVS на x86 можно использовать только регистры ESI и EDI, что приводит к куче лишних MOV и сводит на нет все преимущества.
2. Есть shadow registers.
3. AMD64 много чего добавила.
Ну и вообще там всё сложно может быть.
Руками asm код править можно, но не всегда целесообразно.
Оказалось выгоднее делать RISC ядро + переводчик команд.
С учётом того, что 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 подробно исследовал…
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.
Например, в таком 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
RISC, наоборот, принципиально уходит от ортогональности, нагружая автора кода (и человека, и компилятор) тем, что он не может произвольно сочетать всё со всем, и вообще сильно меньше может: сложные многоэтапные адресации — в топку, операции память-память — к ногтю, адресации больше чем с двумя регистрами — с корнем, и так далее. Зато исполняется полученное легче и дешевле (не нужно трансляции, проще блоки синхронизации внутри конвейера...)
Вот 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 никто бы и не знал.
> при написании программ возникает в основном вопрос «а как на этом чуде вообще хоть что-то написать»?
Ну я в школе на нём целый Форт наваял… но были вдохновение и новизна, да. Сейчас буду только плеваться на такие идеи.
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 — в одну.
Но да, это уже «читерство», конечно.
Ну и хранение константы где-то рядом приводит к тому, что в кодовых секциях появляются данные — как раз для анализа данной статьи может быть жуткой диверсией :)
Если правильно понял — MOV #0, dst как раз заменится на MOV R3, dst.
Если не верите, проверьте сами на gcc.godbolt.org
Что-то мне подсказывает, что все мультимедиа расширения, начиная с MMX в коде компилятора встретить сложно, ибо они не про то вообще. Кстати, было бы интересно проверить эту гипотезу
pxor
в clang-10 встречается 5933 раза, pcmpeqb
— 5193 раза, и т.д.(Нет, инструкций AVX пока ещё не больше, чем всего остального вместе взятого, а примерно 30% от общего числа. Об этом был мой предыдущий пост.)
software.intel.com/sites/landingpage/IntrinsicsGuide/#techs=AVX_512
это разбивка 512-го на семантически разные — с точки зрения интела — куски. предположим, мы поделим это на три (с учетом разной ширины регистров), всё равно это больше тысячи функционально разных операций.
В 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 разных мнемоник не набрал :-)
Для компилированного 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 и работу с вариадиками в код.
Хм, этого не видел. Но они тоже должны отловиться теми же методами.
Я думал наоборот должно быть гораздо больше
Самих инструкций гораздо меньше.
Вот в 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 могут быть разными инструкциями?
А с другой стороны 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 там же задаётся условие.Вполне возможно, что в 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 — т.е. на порядок больше, чем «пара десятков».
А где-то видел статью, где всё это подробно разбиралось.
Подробный разбор тут: www.pagetable.com/?p=39
Так как это всё — не из чтения документации, а из исследования чипа под микроскопом, то сложно сказать кто прав… достаточно чип повернуть на 90 градусов — и строки станут столбцами, а столбцы строками.
На фотографии по вашей ссылке, где эта «таблица» подкрашена зелёненьким, строк ну явно сильно меньше, чем столбцов кстати.
Но речь идет о подсчете количества инструкций, в этом случае я все еще считаю, что команда с разными регистрами считались как разные инструкции.
По вашей же ссылке, где указывается, что добавлено 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.
А вот если бы там была зависимость от регистра, то каждую надо было бы умножать на 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 и получится.
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 байтные инструкции.
В общем я не думаю, что имеет смысл спорить на эту тему. Думаю никто не заморачивался стандартизацией как правильно подсчитывать кол-во инструкций в процессоре, поэтому значение может отличаться в разных документациях.
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 — конкретная инструкция
А вот уже в 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 кб хватит всем" и нет смысла байты экономить, то ли стало нравится решать прикладные задачи.
Вы сегодня можете себе представить, чтобы с одной платформой сегодня возились столько лет и копали так глубоко? Причём не в одиночку, а тысячими, совместно?
Просто «соблазнов» стало больше, уже на ассемблер и машинные коды стало не хватать терпения…
Ну я тоже копал еггоги :)
А что ещё делать, когда это единственная толком доступная электронная игрушка, и интересно, как она работает?
Было бы что-то поприличнее и поконструктивнее (хотя бы с постоянной памятью! МК-52 был дорог и редок) — занимались бы чем-то менее специфическим.
> Причём не в одиночку, а тысячими, совместно?
А сколько человек, по-вашему, занимаются тонкостями работы процессоров x86? Явно ещё больше, причём не только из чисто поржать :)
> уже на ассемблер и машинные коды стало не хватать терпения…
Вот вполне хватает.
А сколько человек, по-вашему, занимаются тонкостями работы процессоров x86? Явно ещё больше, причём не только из чисто поржать :)Вот только этих процессоров — десятки. Какая нибудь ALTINST со своим «подземельем» (причём более глубоким, чем у МК-52) — только на C3 есть.
Вот вполне хватает.Да ладно? Даже никто не составил карту незадокументированных инструкций, не вызывающих #UD, по моделям! Это, извините, дюже халтурная Еггогология.
Хватает, максимум, инструкцию почитать…
Но это потому что, Aarch32 и Aarch64 имеют между собой мало общего и описываются, фактически, отдельно.
Я потому, кстати, и сказал «почитать», а не «прочитать». Не знаю — читал ли все эти тома хоть кто-нибудь «от корки до корки».
Возможно ещё другой вариант сработал: стал программированием зарабатывать, сдельно, по результату, а не на окладе, и стала больше интересовать скорость достижения результата чуть выше чем приемлемая для заказчика. Когда вопрос для себя стоит сдать задачу сейчас и получить деньги сразу, или сдать через неделю и получить те же деньги, максимум денёк-другой (точнее процентов 5 от общих затрат времени) отдашь на невидимые (или незначимые типа потребляемой памяти) заказчику улучшения.
В i386/x86_64 есть SSE3/4.1/4.2/AVX инструкции?
Для параллельных систем понятно, что чем больше распараллеливания, тем проще обработка.
Для процессорных с одной стороны нет разницы, но вот если задуматься… По сути набор одинаковых инструкций задействует одни и те же цепи логических элементов, соответственно тепловыделение данных микрозон будет больше. С другой стороны, если будет задействовано максимальное разнообразие инструкций, с частой сменой логических последовательностей между тактами, нагрев микрозон процессора будет меньше и теплоотвод увеличится (теоретически) и получится некое распределение инструкций. Соответственно меньше температура -> больше частота (меньше троттлинг).
И я бы не сказал, что небольшое количество быстрых инструкций лучше — i386 и x86-64 до сих пор занимают 100% десктопных PC. Почему? Наверное потому что они лучше. А всякие АРМы удел мобильных систем, потому что там очень нужна экономия заряда (и то половина мобильных ПК остаются на CISC архитектуре с огромным успехом).
А вот настоящие CISCи, типа VAX-11 проиграли свою войну давным давно и исчезли. И в основном потому, что CISC хорош для ручного написания кода на ассемблере, а RISC — для кода, сгенеренного компилятором.
PS Я исхожу из того что — RISC — это идея, а не слово, которое ARM вставил к себе в название
ARM чётко понял что именно круто в RISC: инструкции одинаковой длины. И то — со временем Thumb32 от этого отошёл.
Всё остальное — было проигнорировано уже в ARM1 (экспериментальная версия, никогда не продававшаяся).
P.S. Ну и, конечно, ARM также понял что именно не круто в RISC: «рыхлый код». Отказ ARM от классического RISC был как раз не «непониманием», а куда более глубинным пониманием — не только преимуществ RISC, но и недостатоков тоже.
push
по одному регистру.А 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
. Предкомпилированные дистрибутивы собираются с максимально совместимыми наборами инструкций.
Вроде требовал 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’ах.
Сходу нашел такой пример:
https://github.com/xoreaxeaxeax/movfuscator
Объясните неумному:
установил для своего ARM64 приложение из apt. У меня есть поддержка NEON, а вот приложение из apt собрано с поддержкой NEON? Наверное нет, а ведь есть ещё куча менее распиаренных расширений (cx16, SSSE3 и др), получается, надо бы всё пересобирать из исходников на целевую машину? Звучит утопично...
А так — да, код всех программ в дистрибутике собирается под общую гарантированную базу.
Во времена 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 памяти?
Не, теоретически там это всё можно даже запустить… и даже выяснить — окроется ли там эта страничка Хабра за день или пара недель потребуется… но смысл?
Смысл — есть. Особенно — для серверов.
Но Пентиумы и селероны будут в пролёте.
Сделать две версии — обычную X86-64 и продвинутую с AVX2 — достаточно просто.
Кстати, AVX512 пока что медленнее, чем AVX2, из-за перегрева камня и снижения частоты.
Кстати, AVX512 пока что медленнее, чем AVX2, из-за перегрева камня и снижения частоты.Ну не, это всёж-таки неправда. Там хитрее ситуация.
Как только вы включаете AVX512 — у вас сразу падает частота. Но если вы сможете, при этом, эффективно задействовать «широкие» векторные инструкции в целых 64 байта шириной — то вы можете отыграться и получить даже 60-70% ускорения. А вот если у вас нужно ещё и «узкие» данные обрабатывать, наряду с «широкими»… то может получиться даже замедление.
Так что смысл от AVX512 есть, но… не всегда.
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.
Сколько инструкций процессора использует компилятор?