Сколько инструкций процессора использует компилятор? +89




Месяц назад я попытался сосчитать, сколько разных инструкций поддерживается современными процессорами, и насчитал 945 в Ice Lake. Комментаторы затронули интересный вопрос: какая часть всего этого разнообразия реально используется компиляторами? Например, некто Pepijn de Vos в 2016 подсчитал, сколько разных инструкций задействовано в бинарниках у него в /usr/bin, и насчитал 411 — т.е. примерно треть всех инструкций x86_64, существовавших на тот момент, не использовались ни в одной из стандартных программ в его ОС. Другая любопытная его находка — что код для x86_64 на треть состоит из инструкций mov. (В общем-то известно, что одних инструкций mov достаточно, чтобы написать любую программу.)

Я решил развить исследование de Vos, взяв в качестве «эталонного кода» компилятор LLVM/Clang. У него сразу несколько преимуществ перед содержимым /usr/bin неназванной версии неназванной ОС:

  1. С ним удобно работать: это один огромный бинарник, по размеру сопоставимый со всем содержимым /usr/bin среднестатистического линукса;
  2. Он позволяет сравнить разные ISA: на releases.llvm.org/download.html доступны официальные бинарники для x86, ARM, SPARC, MIPS и PowerPC;
  3. Он позволяет отследить исторические тренды: официальные бинарники доступны для всех релизов начиная с 2003;
  4. Наконец, в исследовании компиляторов логично использовать компилятор и в качестве подопытного объекта :-)

Начну со статистики по мартовскому релизу LLVM 10.0:
ISA Размер бинарника Размер секции .text Общее число инструкций Число разных инструкций
AArch64   97 МБ 74 МБ 13,814,975 195
ARMv7A 101 МБ 80 МБ 15,621,010 308
i386 106 МБ 88 МБ 20,138,657 122
PowerPC64LE 108 МБ 89 МБ 17,208,502 288
SPARCv9 129 МБ 105 МБ 19,993,362 122
x86_64 107 МБ 87 МБ 15,281,299 203
В прошлом топике комментаторы упомянули, что самый компактный код у них получается для SPARC. Здесь же видим, что бинарник для AArch64 оказывается на треть меньше что по размеру, что по общему числу инструкций.

А вот распределение по числу инструкций:


Неожиданно, что код для SPARC на 11% состоит из nop-ов, заполняющих branch delay slots. Для i386 среди самых частых инструкций видим и int3, заполняющую промежутки между функциями, и nop, используемую для выравнивания циклов на строки кэша. Наблюдение de Vos о том, что код на треть состоит из mov, подтверждается на обоих вариантах x86; но даже и на load-store-архитектурах mov оказывается если не самой частой инструкцией, то второй после load.

А как набор используемых инструкций менялся со временем?

Единственная ISA, для которой в каждом релизе есть официальный бинарник — это i386:


Серая линия, отложенная на правой оси — это число разных инструкций, использованных в компиляторе. Как мы видим, некоторое время назад компилятор компилировался гораздо разнообразнее. int3 стала использоваться для заполнения промежутков только с 2018; до этого использовались такие же nop, как и для выравнивания внутри функций. Здесь же видно, что выравнивание внутри функций стало использоваться с 2013; до этого nop-ов было гораздо меньше. Ещё интересно, что до 2016 mov-ы составляли почти половину компилятора.

Самые первые версии LLVM, до появления clang, выпускались ещё и с бинарниками для SPARC. Потом поддержка SPARC утратила актуальность, и вновь она появилась лишь через 14 лет — с на порядок увеличившимся числом nop-ов:


Исторически следующая ISA, для которой выпускались бинарники LLVM — это PowerPC: сначала для Mac OS X и затем, после десятилетнего перерыва, для RHEL. Как видно из графика, переход после этого перерыва к 64-битному варианту ISA сопровождался заменой большинства lwz на ld, и вдобавок удвоением разнообразия инструкций:


В бинарниках для x86_64 и ARM частота использования разных инструкций почти не изменялась:




При подсчёте инструкций ARM я отсекал суффиксы условий — вместе с ними получалось около тысячи разных инструкций, но даже и без них ARM сильно опережает другие ISA по разнообразию генерируемых инструкций. Таким образом, слой b на этом графике включает и все условные переходы тоже. Для остальных ISA, где условными могут быть только переходы и немногие другие инструкции, суффиксы условий при подсчёте не отсекались.

Наконец, самая недавняя ISA, для которой публикуются официальные бинарники — это AArch64. Здесь интересно то, что orr с прошлого года почти перестала использоваться:


PowerPC и AArch64 оказались единственными ISA, для которых число разных используемых инструкций всё растёт и растёт.




Комментарии (219):

  1. sinc
    /#21744488

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

    • tyomitch
      /#21744496 / +3

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

      • voidnugget
        /#21745360

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


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

        • beskaravaev
          /#21745784

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

          • voidnugget
            /#21746052 / +2

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


            Так в языке


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

            • uncle_goga
              /#21746552

              Я так понимаю вы собираетесь в перспективе написать ЯП? Если это так, то было бы здорово услышать все размышления на эту тему

              • voidnugget
                /#21746596

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

            • 0xd34df00d
              /#21749006

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

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


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


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

              • voidnugget
                /#21749372

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

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


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

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


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

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


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

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

                • 0xd34df00d
                  /#21751684

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

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


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

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

                  • voidnugget
                    /#21752036

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

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


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


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

      • qw1
        /#21745690

        Некоторые инструкции существовали для удобства программистов на ассемблере (типа rep cmpsw), потребность в них падает.
        С другой стороны, «обычные», часто используемые инструкции оптимизируются производителями процессоров так, что можно оставаться на обычных и отказаться от экзотических без потери скорости (типа leave/enter для пролога/эпилога функций)

        • khim
          /#21746434

          Тольво вы путаете «можно» и «нужно». Гляньте в табличку. Если LEAVE просто тормозит (3 тика на Ryzen для того, что она делает — это много), то ENTER сегодня — это «гроб с музыкой»: 16 тактов, за это время можно штук 20-30 обычных MOV исполнить!

          Исселование, подобное тому, что делает автор разработчики процессоров тоже делают… и в результате на современных процессорах разные экзотические инструкции не то, что бессмысленно использовать — их вредно использовать! Даже руками на ассмеблере! Скорость будет никакая!

      • Calc
        /#21746104

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

        • khim
          /#21746468 / +1

          Там не всё так просто. Поскольку компиляторы «экзотику» не используют, то на неё «подзабили»: инструкции есть, но поддерживаются «на отвяжись».

          Одно время «забили» даже на MOVS и разработчики упражнялись в подборе оптимального набора SSE инструкций. А в Ivy Bridge «случилось чудо»: внезапно старая добрая MOVSB была оптимизирована и стала работать быстрее любых SSE (для больших объёмов: 256 байт и больше). Интересно — умеет это использовать LLVM или нет…

          • Tiriet
            /#21746600

            странно. По Вашей ссылке 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

            • khim
              /#21746618

              А моё замечание в скобочках вы проигнорировали?

              • Tiriet
                /#21749554

                наверно, я просто неправильно понял «few hundred bytes»- мне казалось, что «несколько сотен»- это больше, чем три сотни (256), а для больших объемов- вроде и так понятно, что раз оно не ложится в один запрос к памяти- то как ни оптимизируй эту инструкцию, скорость ее все равно упрется в ПСП, которой все равно, как оно там в процессоре реализовано, хоть через SSE, хоть через IA-32.
                На числодробилке я не вижу особой разницы между movdqa, movapd, movupd и movsd, когда надо больше, чем «few hundred bytes» прожевать: все выдают ~12GB/s.

          • CoolCmd
            /#21746784

            а я помню, что movsb быстро работала уже в sandy bridge

            • khim
              /#21747202

              Она там работала гораздо быстрее, чем у AMD, но медленнее, чем SSE-мувы.

              • CoolCmd
                /#21747558

                не в курсе, как сейчас с этим у рязани?

                • khim
                  /#21747580

                  Нет, не в курсе. Но это не так важно: процессоры, которые такое умеют должны выставлять специальный CPUID бит.

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

                  • CoolCmd
                    /#21747618

                    что умеет? movsb была еще в 8086.

                    • thealfest
                      /#21748356 / +2

                      Есть такой флаг в CPUID:
                      Enhanced REP MOVSB/STOSB

                    • khim
                      /#21748604

                      «Умеет в MOVSB, который быстрее SSE».

                  • Maccimo
                    /#21747836

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

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

      • MooNDeaR
        /#21747122 / +1

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

        • Psionic
          /#21748906 / +1

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

        • tbl
          /#21751196

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

          • khim
            /#21751544

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

          • Psionic
            /#21752040

            А как обвязка зависимостями сторонними зависимостями на этот подход влияет? Одно дело скомпилить один пакет, а другое дело скомпилить все древо заисимостей, что вообще не всегда возможно, а когда возможно то может занять пару/тройку десятков часов чистого времени.

            • 0xd34df00d
              /#21752190

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

              • tbl
                /#21752238

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

      • vvmtutby
        /#21747660

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

        «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).

    • kdmitrii
      /#21744512 / +7

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

      • sinc
        /#21744542 / +2

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

        • Srgun
          /#21745878 / +1

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

          • khim
            /#21746470

            Могут… но не дают. C++ всё ещё быстрее — даже без использования экзотики.

            Другое дело, что когда выбором заведуют маркетологи реальные цифры никого не волнуют, главное — красивые лозунги.

          • sergegers
            /#21747710

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

            • khim
              /#21748606 / +2

              Ага. А перед этим он смотрит, чтобы процессор был от Intel, не AMD, да.

              • sergegers
                /#21748818

                Ну да, есть такое, расширенные инструкции у AMD он не использует.

                • VolCh
                  /#21749126

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

                  • Srgun
                    /#21749352

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

                  • khim
                    /#21750366

                    Никакие. Проверка на производителя процессора идёт в самом начале.

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

                  • sergegers
                    /#21751370

                    Да. Игнорируется поддержка SSE2, SSE3.

        • 0xd34df00d
          /#21749010

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

      • fougasse
        /#21744546 / +2

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

        • Gryphon88
          /#21746066

          Возможно, интеловский или иной коммерческий компилятор использует больше инструкций.

  2. drWhy
    /#21744650 / -1

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

    • sumanai
      /#21747304

      Любой хром стремится полностью занять ОЗУ, это факт.

  3. Mingun
    /#21744702 / +3

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

    • tyomitch
      /#21744730

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

      • Mingun
        /#21745126

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

        • khim
          /#21746478 / +2

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

          А вот встречаются ли они хоть где-то, кроме тестовых программ — вопрос интересный.

      • Alexey_Alive
        /#21749988

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

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

    • crea7or
      /#21744766 / +2

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

      • Mingun
        /#21745122 / +3

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

        • crea7or
          /#21745310

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

          • VolCh
            /#21745588

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

          • Mingun
            /#21746156

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

            • crea7or
              /#21747872 / +1

              исследовать такой код тяжело, проще спросить разработчиков (если они конечно согласятся что-то прояснить).

    • khim
      /#21746488

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

    • 0xd34df00d
      /#21749016

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


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


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


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

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

      • Tiriet
        /#21749596

        А как вообще компилятор может понять, какие входные данные будут поданы Вами на вход программы? никак.

        • Alexey_Alive
          /#21750034

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

          • tyomitch
            /#21750118

            На уровне языка такая возможность есть:

            void foo(int bar) {
                if (bar < 0 || bar > 42) *(int*)0;
                ...
            
            --означает «мамой клянусь, bar будет от 0 до 42».

            Что с такими указаниями делает компилятор — это другой вопрос.
            В ряде случаев clang умеет ими пользоваться: godbolt.org/z/AbQk-p

            • Alexey_Alive
              /#21750428

              Ну это хак, а не фича языка. Да и хотелось бы, чтобы в C++ была возможность типам диапазоны значений указывать.

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

          • Tiriet
            /#21750452

            ааа, ну так std:string- это же, емнип, по стандарту и есть nullterminated string, так что это-то Ваш компилятор как раз знает :-).

            • tyomitch
              /#21750468

              Неправда: std::string — это произвольный массив байт, и он может содержать любые символы, в т.ч. '\0'

              • Tiriet
                /#21751078 / +1

                формально- да. но стд считает, что работать Вы с этим «произвольным массивом» будете именно как с нуль-строкой.

                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 ему прямо об этом и говорит. Если нет- то есть и другие контейнеры.

                • tyomitch
                  /#21751484

                  Это потому, что вы используете конструктор из const char*.

                  std::cout << std::string("Test string\0 2222", 18); напечатает всю строку вместе с нулём, и итерироваться она будет вместе с нулём, и length() вернёт 18.

                  • Gryphon88
                    /#21751878

                    Мне что-то вспомнилась дискуссия про нуль-терминированные строки и паскалевские строки, начинающиеся с 8 бит длины строки, в результате которой родился мерзкий гибрид, у которого в начале октет длины. а в конце \0. Вроде этот способ является основным в Обероне.

                    • qw1
                      /#21752758 / +1

                      А что в этом плохого? std::string так и хранит — длину отдельно, а в конце терминирующий nul, чтобы и c_str(), и length() работали за O(1).

                      • Gryphon88
                        /#21754330

                        Отдельно — это нормально, мне не нравится, что в гибриде в первом элементе массива вместо char uint8_t. Ну и тут или длина, или терминатор, всё-таки c_str и std::string разные вещи, не совсем понимаю стремление плюсов к обатной совместимости с чистым С. В 98м это ещё смысл имело.

                        • khim
                          /#21755204

                          И сейчас, к сожалению, имеет. Ибо у C++ тупо нет некоторых вещей, которые реализуются через C API — либо C++версии банально менее популярны, либо их вообще нету…

                          Причём, внезапно, очень много всего, связанного со строками: GetText или LibXML2 какие-нибудь…

                        • tyomitch
                          /#21759046

                          Как в 98м имела смысл совместимость с чистым Си, так в 2020 имеет смысл совместимость с С++98.
                          Это как в басне про шатл и лошадиную задницу.

                  • Tiriet
                    /#21753856 / -2

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

                    • netch80
                      /#21754000

                      > Потому что стд предполагает, что 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 не имеет смысла — там длина хранится всегда.

                    • 0xd34df00d
                      /#21755528

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

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


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

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


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

  4. mwizard
    /#21744770 / +2

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

    • Nagg
      /#21744884 / +2

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

  5. a-tk
    /#21744828

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

  6. sim2q
    /#21744916 / +1

    а теперь отрезать неиспользованное и получить RISC посмотреть сколько площади освободиться

    • snizovtsev
      /#21746390

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

    • Alexey_Alive
      /#21750050

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

  7. Siemargl
    /#21744962

    Заметил (давно), что i387 инструкции больше не используются. Это учтено?

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

    Ну и было бы неплохо добавить в статистику число тиков на инструкцию как ИТОГО… Архитектуры то разные.

    • tyomitch
      /#21744994

      Ну например, fstp в clang-10 встречается 1693 раза, fld — 1235 раз.
      Я бы это не назвал «больше не используются».

      • Siemargl
        /#21745024 / +1

        А в 64-бит версиях?

        Я встречал мнения, что 32бит покинуты (abandoned), т.е не развиваются.

        • tyomitch
          /#21745068

          Вы правы: в версиях для x86_64 инструкций i387 с самого начала было очень мало, а после версии clang-3.5 (2014) их осталось меньше десятка на весь бинарник.

  8. eastig
    /#21745000

    1. Большое количество MOVs и LOADs — это несовершенство аллокатора регистров и генерация кода для Phi-функций SSA представления программы.
    2. Компилятор не использует все возможные инструкции, потому что чтобы их применить нужно обнаружить соотвествующие операции в исходной программе. Внутреннее представление (IR) программы в компиляторе (пример llvm.org/docs/LangRef.html) в основном состоит из простых операций, которые чаще всего один в один отображаются в target ISA. Добавление всевозможных сложный операций в IR усложняет написание платформенно-независимых оптимизаций. А в кодо-генераторе полно других важных проблем, которые нужно решать.

    • Nagg
      /#21745728 / +1

      1. Верно, но это сложно как-либо оценить статистикой, т к даже с идеальным регистр аллокатором мувы все равно будут
      2. В LLVM есть пяток проходов, где он пытается по набору операторов распознать всякие интринсики, например https://godbolt.org/z/yRJubW

    • ajojo
      /#21746364

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

      • eastig
        /#21746548 / +1

        Простые с точки зрения разработчика компилятора. А именно, каждая инструкция LIR делает только одно определенное действие, не имеет неявных зависимостей и обладает минимумом побочных эффектов. Инструкция явно предоставляет всю информацию о себе, что позволяет писать оптимизации на основе matching, пример InstrCombine pass. Наличие типов как раз упрощает их.
        Если бы в IR были бы такие сложные x86 инструкции как 'LOOP' или 'REP CMPS', то всем оптимизациям приходилось бы каждый раз иметь в виду что инструкция делает больше чем одно действие. Это все затрудняет написание generic оптимизаций. Кстати интринсики в IR — это как раз и есть возможность использовать сложные инструкции. Только вот оптимизации не любят интринсики.
        По поводу различных мнемоник, как раз простота IR позволяет автоматизировать процесс мэпинга инструкций IR в машинные инструкции. В LLVM за это отвечает tablegen, который берет описание ISA и генерирует таблицу конечного автомата.

        • ajojo
          /#21746710

          это всё прекрасно, что вы рассказываете, но никаким 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>]

          • eastig
            /#21746788 / +1

            А что в вашем понимании 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.»

            • ajojo
              /#21746952

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

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

              • eastig
                /#21747124 / +1

                В теории может, но на практике я этого в 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 файлов описаний. Затем мы реализовывали специфичные оптимизации, и то если в этом есть необходимость.

  9. nerudo
    /#21745022 / +1

    Как-то странно смотрится большее количество инструкций в x86 против ARM в контексте CISC vs RISC. Либо функционально бинарники не эквивалентны?

    • Siemargl
      /#21745032

      CISC по определению имеет больше инструкций чем RISC (reduced instruction set computer)

      • nerudo
        /#21745038 / +1

        Ээээ… Больше разных инструкций в ISA => больше функционала на одну инструкцию => меньше количество инструкций в коде. Нет?

        • Siemargl
          /#21745064

          Даже с моим минимальным опытом, я бы сказал, что весьма неоднозначно.

        • agalakhov
          /#21745170

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

          • BD9
            /#21748688

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

            Ну и вообще там всё сложно может быть.
            Руками asm код править можно, но не всегда целесообразно.

            • qw1
              /#21748750

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

              • BD9
                /#21748778

                Есть переименование регистров внутри процессора самим процессором.

                • Alexey_Alive
                  /#21750082 / +1

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

        • BD9
          /#21748708

          Intel ушла от этого начиная с i486.
          Оказалось выгоднее делать RISC ядро + переводчик команд.

          • khim
            /#21748742 / +1

            Всё-таки с Pentium Pro, а не с i486. И i486 и даже Pentium исполняют x86 инструкции «напрямую».

            • BD9
              /#21748748

              С Википедии:

              Intel 80486 (также известный как i486, Intel 486 или просто 486-й) — 32-битный скалярный x86-совместимый микропроцессор четвёртого поколения, построенный на гибридном CISC-RISC-ядре и выпущенный фирмой Intel 10 апреля 1989 года.

              • khim
                /#21748822 / +1

                В английской этого нет. Кто и что понаписал в русской — я не в курсе. ?opsы — это P6.

                • tyomitch
                  /#21748900

                  Как вам такой источник? books.google.com/books?id=nceYr-Zzca8C&pg=PA10&dq=risc+core

                  • khim
                    /#21748936 / +1

                    То что вы откопали — это рекламная статья, причём не от Intel, а от кого-то, кто что-то где-то, по слухам, узнал — причём не о 80486, а о будущем P5.

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

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

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

                    • tyomitch
                      /#21749902

                      Я про то, что это не википедисты выдумали: такое действительно писали.
                      На сайте Падуанского университета тоже висит такое в материалах по курсу Advanced Computer Architectures.

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

                      • khim
                        /#21750408

                        Особенно при наличии публикаций о том, что оно якобы есть.
                        Записать рекламный мусор в «неавторитетные источники» и потребовать ссылки на технический мануал?

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

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

                        Иногда такие замечания пытаются «отшить» объясняя, что никаких других мануалов у нас и нету… в случае iAPX 432 это, может быть, даже и оправдано, но когда есть подробные исследован ия микроархитектуры. Тот же Agner Pentium и Pentium Pro подробно исследовал…

                        • BD9
                          /#21752536 / +1

                          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.

                          • beeruser
                            /#21752646

                            но работа подобна RISC.

                            Камень подобен сердцу человеческому и в нём заключен кристалл сияющий!(с)

                          • khim
                            /#21752648

                            Декодера 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.

                            • BD9
                              /#21760726 / +1

                              В те годы RISC была «стильно-модно-молодёжной» технологией, поэтому производители прикручивали эти красивые буковки ко своему товару. Считалось, что RISC — это «простые инструкции за 1 такт», чему i486 соответствует отчасти.

      • netch80
        /#21748268 / +1

        Нет такого определения. 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, у которого максимум один операнд в памяти (строковые не в счёт), требует тут трансляции.

        • tyomitch
          /#21748362

          x86, у которого максимум один операнд в памяти (строковые не в счёт)

          В прошлом топике напомнили ещё и про push [addr]
          :-P

          • netch80
            /#21748376

            Ну отдельные подобные исключения, да, есть. Спасибо за подсказку.

        • Misaka10032
          /#21748788

          Насчёт PDP-11 — а разве это не просто следствие ортогональности системы команд? Я не большой знаток архитектур, но в таком RISC, как MSP430, система команд так же ортогональна и тот же mov делает так же кучу вещей.

          • netch80
            /#21749050

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

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

            • khim
              /#21750436

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

              • netch80
                /#21750482

                Так много чего можно отнести к совершенно разным классам одновременно.

                Вот 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 никто бы и не знал.

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

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

          • netch80
            /#21757436

            Кстати, посмотрел я на доки по 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 — ой нет ;(

            • khim
              /#21757664 / +1

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

              Но да, это уже «читерство», конечно.

              • tyomitch
                /#21759050 / +1

                Интересно, какой из этих способов быстрее, или же на разных чипах по-разному.

                • netch80
                  /#21759248

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

            • Misaka10032
              /#21759464

              Так по поводу MOV #0, dst и MOV R3, dst — в MSP430 есть 2 хитрых регистра, генераторы констант — R2 (который ещё и Status Register) и R3.
              Если правильно понял — MOV #0, dst как раз заменится на MOV R3, dst.

    • beeruser
      /#21745054 / +2

      А что странного? ARMv8 ISA более выразительная и обычно требуется меньше инструкций для аналогичного кода. Часто код ещё и меньше занимает.
      Если не верите, проверьте сами на gcc.godbolt.org

    • eastig
      /#21745074

      Да у них есть различия в функционале.

      • tyomitch
        /#21745090

        А конкретнее?

        • eastig
          /#21745200

          В исходниках LLVM eсть немного "#if defined(__i386__) || defined(__x86_64__)".
          Но основное различие в том, поддержку каких targets включили при построении Clang/LLVM. Если все бинарники с llvm.org построены с одними и теми же настройками, то отличия должны быть минимальны.

  10. Elmot
    /#21745042

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

    • Siemargl
      /#21745072 / +1

      Для x64 наоборот — в основном SSE.

    • tyomitch
      /#21745086 / +1

      Не подтверждается: pxor в clang-10 встречается 5933 раза, pcmpeqb — 5193 раза, и т.д.

      • ajojo
        /#21746000

        А выше чем SSE? А, к примеру, AVX'ы, которых по числу инструкций как бы не больше, чем всего остального вместе взятого?

        • tyomitch
          /#21746010

          SSE2 используется, AVX нет.

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

          • ajojo
            /#21746326 / +1

            вы считали AVX только по мнемоникам, я так понимаю? но в 512-м есть, во-первых, маскирование с двумя режимами. есть отсутствие маскирование (вырожденная маска), есть broadcasting bit, который тоже меняет семантику. посмотрите сюда:

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

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

      • ajojo
        /#21746386

        подождите, а не подтверждается что именно? для pxor и pcmpeqb есть оба варианта. вы смотрели на их аргументы? я сомневаюсь, что современные компиляторы генерят код для mmx — медленно и регаллоку лишняя головная боль.

        • tyomitch
          /#21746486

          Не подтверждается именно то, на что я отвечал: «что все мультимедиа расширения, начиная с MMX в коде компилятора встретить сложно»

          • ajojo
            /#21746646

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

    • netch80
      /#21748332

      Вопрос в том, что встречать расширениями мультимедиа :)

      В 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% всех команд это немало.

      • tyomitch
        /#21748338

        objdump -d libclang-cpp.so.10

        У вас здесь та же неточность, что и у Pepijn de Vos: вы дизассемблируете не только .text, но и неисполнимые данные.
        На одном только исполнимом коде он бы 411 разных мнемоник не набрал :-)

        • netch80
          /#21748374 / +1

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

          • khim
            /#21748622 / +1

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

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

            • netch80
              /#21749028

              > в типичной программе этого добра действительно немного

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

              Я прошёлся после вчерашнего вопроса 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 и работу с вариадиками в код.

              Хм, этого не видел. Но они тоже должны отловиться теми же методами.

              • khim
                /#21750458

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

                Но я не знаю — отправили они правки в upstream или нет.

  11. saboteur_kiev
    /#21745192

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

    • beeruser
      /#21745282

      Это пермутации всех регистров.
      Самих инструкций гораздо меньше.
      Вот в ARMv1 лишь 45 различных инструкций и 23 мнемоники.
      en.wikichip.org/wiki/arm/armv1

      • saboteur_kiev
        /#21748916

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

        • beeruser
          /#21751380

          Машинный код сформирован по простым правилам — есть поля кода операции, регистров и непосредственных данных.

          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

          • saboteur_kiev
            /#21752698

            Например
            LD DE, xxxx — 11xxxxh — 00010001 0000000000000000
            LD A, xxxxh — 3axxxxh — 00011101 00000000000000000
            LD A, xxh — 3exxh — 00011111 000000000

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

            • khim
              /#21752726

              Если вы про Z80, то в первоисточнике первая инструкция называется LXI, вторая LDA, а третья MVI… (если не напутал).

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

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

              • saboteur_kiev
                /#21756206

                Ваша ссылка ведет просто на статью в википедии, где про инструкции ни слова…

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

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

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

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

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

                • khim
                  /#21756290

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

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

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

                  • saboteur_kiev
                    /#21758544

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

                    • khim
                      /#21758572

                      Суть в том, что если разный 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. Странно только что вы об этих самых-самых азах не знаете. В том смысле, что если вам это всё неинтересно… то почему вы вообще об этом пишите? А если интересно — то информации ж вагон (в том числе на Хабре), зачем же выдумывать?

                      • tyomitch
                        /#21759064

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

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

                        • khim
                          /#21759540

                          130 — это количество ячеек в этой таблице, а не количество строк.

                          А где-то видел статью, где всё это подробно разбиралось.

                          • tyomitch
                            /#21759590

                            Нет же, 130 строк по 21 биту в строке.
                            Подробный разбор тут: www.pagetable.com/?p=39

                            • khim
                              /#21759614 / +2

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

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

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

                      • saboteur_kiev
                        /#21759154

                        Это понятно, что группы инструкций типа 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.

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

                        • netch80
                          /#21759272 / +1

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

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

                          • khim
                            /#21759608 / +1

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

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

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

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

                          • saboteur_kiev
                            /#21759792

                            Текущий мануал же явно отличается от того, когда 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 байтные инструкции.

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

            • beeruser
              /#21753676

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

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

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

              Но когда вы говорили про 1200 инструкций, вы считали
              LD a,n LD b,n LD c,n LD e,n LD d,n и т.д. как разные инструкции, а она одна.

              • saboteur_kiev
                /#21754344

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

                Я был неправ?

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

                • VolCh
                  /#21754824

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

                  • khim
                    /#21755292

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

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

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

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

                    • VolCh
                      /#21755782

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

                      • khim
                        /#21755830

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

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

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

                        • netch80
                          /#21756448

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

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

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

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

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

                          Вот вполне хватает.

                          • khim
                            /#21757362

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

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

                            Хватает, максимум, инструкцию почитать…

                            • Gryphon88
                              /#21757594

                              Справедливости ради, инструкция там хорошо за 4к страниц.

                              • khim
                                /#21757670

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

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

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

                        • VolCh
                          /#21756920

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

  12. rogoz
    /#21745278

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

    • tyomitch
      /#21745950

      Возможно i386 тоже расширен до SSE2.

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

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

      Нет, только до SSE2.

  13. WoLandN
    /#21745468

    Мне вот как разработчику на FPGA интересно было бы провести некоторые параллели или скорее узнать у осведомлённых, что лучше: одна и та же программа с использованием минимального набора инструкций или наоборот максимального с изменением типа инструкции каждый такт (желательно).
    Для параллельных систем понятно, что чем больше распараллеливания, тем проще обработка.
    Для процессорных с одной стороны нет разницы, но вот если задуматься… По сути набор одинаковых инструкций задействует одни и те же цепи логических элементов, соответственно тепловыделение данных микрозон будет больше. С другой стороны, если будет задействовано максимальное разнообразие инструкций, с частой сменой логических последовательностей между тактами, нагрев микрозон процессора будет меньше и теплоотвод увеличится (теоретически) и получится некое распределение инструкций. Соответственно меньше температура -> больше частота (меньше троттлинг).

    • Elmot
      /#21745806

      Как-бы исход противостояния RISC и CISC процессоров подсказывает ответ. В целом мало быстрых но простых инструкций лучше чем много сложных но медленных. Если есть избыточная емкость FPGA, можно фигачить одинаковые блоки и перебрасывать исполнение между ними по кругу, чтобы распределить тепловыделение. А оттуда и до суперскалярности недалече.

      • WoLandN
        /#21746112

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

        • Elmot
          /#21746480

          Современный x86 обеих битностей уже давно не CISC технически. Это гибрид — все частые инструкции хардверные и однотактовые или даже суперскалярные, куча редких, которые тянутся для совместимости — медленные на микрокоде. На ARM та же история, но к тому же сильно завязанная на уровень ядра.
          А вот настоящие CISCи, типа VAX-11 проиграли свою войну давным давно и исчезли. И в основном потому, что CISC хорош для ручного написания кода на ассемблере, а RISC — для кода, сгенеренного компилятором.

          PS Я исхожу из того что — RISC — это идея, а не слово, которое ARM вставил к себе в название

          • tyomitch
            /#21746498

            На тот момент, когда ARM вставил к себе в название это слово — название вполне соответствовало содержимому.

            • khim
              /#21746546

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

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

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

              P.S. Ну и, конечно, ARM также понял что именно не круто в RISC: «рыхлый код». Отказ ARM от классического RISC был как раз не «непониманием», а куда более глубинным пониманием — не только преимуществ RISC, но и недостатоков тоже.

              • tyomitch
                /#21746658

                Это, кстати, реально обидно — что STM у ARM появился уже тогда, а код для i386 до сих пор на 12% состоит из push по одному регистру.

                • khim
                  /#21746830 / +1

                  Ну в AArch64 они от этого отказались, потому что угрохать существенный процент всего массива из 4 миллиардов инструкций вот именно на эти все «серийные» загрузки и выгрузки — как-то некузяво.

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

                  • eastig
                    /#21746898 / +1

                    STM убрали, но добавили LDP/STP: load/store pair.

              • beeruser
                /#21746888 / +2

                Одна инструкция, которая может записать в память 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, тем не менее плотность кода — выше.

                • khim
                  /#21746992 / +1

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

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

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

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

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

                  • beeruser
                    /#21747762

                    я думаю вы AArch64 имели в виду.

                    Да.

          • khim
            /#21746518 / +1

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

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

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

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

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

  14. amarao
    /#21745808

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


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

    • Maccimo
      /#21745934 / +1

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

  15. neumond
    /#21745816

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

  16. OvO
    /#21745930

    Так большая часть программ в /usb/bin/, в основном, и занимается тем, что копирует данные из одного места в другое.

    • tyomitch
      /#21745966

      … как и большая часть программ вообще.

  17. dimorphus
    /#21746802 / +1

    Анализируя /usb/bin/, вы не увидите системных инструкций. Их надо искать в ядре, в несжатом образе. Если вы сидите не на gentoo, собираемой gcc+graphite или clang+polly с какими-нибудь флагами -march=native -Ofast -ftree-vectorize и т.д. с последним процессором, так там и не будет ничего. Потому что дистрибутивы собирают программы с тем, чтобы они работали на как можно большем количестве процессоров. И лишь в очень небольшом количестве программ есть runtime определение доступных процессорных команд. И скорее всего там они ассемблером закодированы, а не компилятором порождаются. Вообще, мне кажется что надежнее грепом пройтись по исходникам компилятора, а не в бинарниках смотреть.

    • BD9
      /#21748772

      У 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% быстрее других линуксов. Щас — не особо быстрее.

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

  18. Kabdim
    /#21747068

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

  19. 25080205
    /#21747434

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

    • LonelyDeveloper97
      /#21747530

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


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

    • qw1
      /#21748038

      Кто-нибудь разбирался, как он делает циклы, не используя само-модифицирующийся код?

        • qw1
          /#21748092

          Интересно, хотя я думал там будет более алгоритмически чистое решение, а не системно-зависимые page faults

          • khim
            /#21748628

            Если там есть page faults, то там вообще инструкций не нужно.

            • qw1
              /#21748676 / +1

              Это тоже интересный проект.

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

  20. Antervis
    /#21747850 / +1

    в режиме по умолчанию компилятор не будет, например, использовать SIMD команды новее SSE2, а их там предостаточно. Также он зачастую не будет использовать устаревшие x86 инструкции.

  21. sakamoto-san
    /#21749690 / -1

    у архитектуры armv7a какая-то особая команда mov, что она выделена красным, хотя у всех остальных синим?

  22. alexeyrom
    /#21750054

    Эти бинарники и не должны использовать многие инструкции, которые поддерживают не все процессоры данной архитектуры. Правильнее (но сложнее) было бы скомпилировать Clang для разных CPU (здесь указано, как получить их список: stackoverflow.com/questions/15036909/clang-how-to-list-supported-target-architectures)

  23. DungeonLords
    /#21756182

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

    • 0xd34df00d
      /#21756328

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

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

    • netch80
      /#21756444

      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 и, если без кросс-компиляции, двое суток непрерывного хруста…

      • khim
        /#21757426

        Как с этим в ARM/64 — не знаю, но вроде бы базовые векторные наборы обязаны присутствовать?
        Нет, но дистрибутив пересобирать не нужно.

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

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

        Не, теоретически там это всё можно даже запустить… и даже выяснить — окроется ли там эта страничка Хабра за день или пара недель потребуется… но смысл?

      • BD9
        /#21760764

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

        • khim
          /#21760798

          Кстати, AVX512 пока что медленнее, чем AVX2, из-за перегрева камня и снижения частоты.
          Ну не, это всёж-таки неправда. Там хитрее ситуация.

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

          Так что смысл от AVX512 есть, но… не всегда.