Rust: Проблема с отслеживанием asm (встроенная сборка)

Созданный на 9 нояб. 2015  ·  111Комментарии  ·  Источник: rust-lang/rust

Эта проблема отслеживает стабилизацию встроенной сборки. Текущая функция не прошла процесс RFC и, вероятно, потребуется сделать это до стабилизации.

A-inline-assembly B-unstable C-tracking-issue T-lang requires-nightly

Самый полезный комментарий

Я хотел бы отметить, что встроенный asm-синтаксис LLVM отличается от синтаксиса, используемого clang / gcc. Различия включают:

  • LLVM использует $0 вместо %0 .
  • LLVM не поддерживает именованные asm-операнды %[name] .
  • LLVM поддерживает различные типы ограничений регистров: например, "{eax}" вместо "a" на x86.
  • LLVM поддерживает явные ограничения регистров ( "{r11}" ). В C вместо этого вы должны использовать регистровые asm-переменные для привязки значения к регистру ( register asm("r11") int x ).
  • Ограничения LLVM "m" и "=m" в основном нарушены. Clang переводит их в косвенные ограничения памяти "*m" и "=*m" и передает адрес переменной в LLVM вместо самой переменной.
  • так далее...

Clang преобразует встроенный asm из формата gcc в формат LLVM перед передачей его в LLVM. Он также выполняет некоторую проверку ограничений: например, он гарантирует, что операнды "i" являются константами времени компиляции,


В свете этого я думаю, что мы должны реализовать тот же перевод и проверку, что и clang, и поддерживать правильный inline asm синтаксис gcc вместо странного LLVM.

Все 111 Комментарий

Возникнут ли трудности с обеспечением обратной совместимости встроенной сборки в стабильном коде?

@ main-- имеет отличный комментарий на https://github.com/rust-lang/rfcs/pull/1471#issuecomment -173982852, который я воспроизводю здесь для потомков:

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

Мы также должны обсудить, действительно ли сегодняшнее asm! () Является лучшим решением, или что-то вроде RFC # 129 или даже D было бы лучше. Здесь следует учитывать один важный момент: asm () не поддерживает тот же набор ограничений, что и gcc. Следовательно, мы можем либо:

  • Придерживайтесь поведения LLVM и напишите для этого документы (потому что мне не удалось их найти). Приятно, потому что позволяет избежать сложности в rustc. Плохо, потому что это запутает программистов, пришедших с C / C ++, и потому, что некоторые ограничения может быть трудно эмулировать в коде Rust.
  • Эмулируйте gcc и просто ссылайтесь на их документы : хорошо, потому что многие программисты уже знают это, и есть много примеров, которые можно просто скопировать и вставить с небольшими изменениями. Плохо, потому что это нетривиальное расширение компилятора.
  • Сделайте что-нибудь еще (как это делает D): много работы, которая может окупиться, а может и не окупиться. Если все сделано правильно, это может значительно превзойти стиль gcc с точки зрения эргономики, при этом, возможно, лучше интегрироваться с языком и компилятором, чем просто непрозрачный blob (здесь много размахивания руками, поскольку я недостаточно знаком с внутренними компонентами компилятора, чтобы оценить это) .

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

Я лично считаю, что было бы лучше сделать то, что Microsoft сделала в MSVC x64: определить (почти) исчерпывающий набор встроенных функций для каждой инструкции asm и выполнить «встроенный asm» исключительно через эти встроенные функции. В противном случае очень сложно оптимизировать код, окружающий встроенный asm, что парадоксально, поскольку многие виды использования встроенного asm предназначены для оптимизации производительности.

Одним из преимуществ инстинктивного подхода является то, что он не должен быть принципом «все или ничего». Вы можете сначала определить наиболее необходимые встроенные функции и постепенно наращивать их. Например, для криптографии, имея _addcarry_u64 , _addcarry_u32 . Обратите внимание, что работа над инстринсами, похоже, уже проделана достаточно тщательно: https://github.com/huonw/llvmint.

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

Встроенные функции хороши, но asm! можно использовать не только для вставки инструкций.
Например, посмотрите, как я создаю заметки в формате ELF в моем ящике probe .
https://github.com/cuviper/rust-libprobe/blob/master/src/platform/systemtap.rs

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

@briansmith

Встроенный asm также полезен для кода, который хочет выполнять собственное выделение регистров / стека (например, голые функции).

@briansmith: да, это отличные причины использовать встроенные функции там, где это возможно. Но приятно иметь встроенную сборку в качестве окончательного вывода.

@briansmith Обратите внимание, что asm!() - это _ своего рода_ надмножество встроенных функций, поскольку вы можете построить последнее, используя первое. (Общий аргумент против этого рассуждения состоит в том, что компилятор теоретически может оптимизировать _through_ встроенные функции, например, вывести их из циклов, запустить CSE на них и т.д. в любом случае, чем компилятор.) См. также https://github.com/rust-lang/rust/issues/29722#issuecomment -207628164 и https://github.com/rust-lang/rust/issues/29722# issuecomment -207823543 для случаев, когда встроенный asm работает, а встроенный - нет.

С другой стороны, встроенные функции критически зависят от «достаточно умного компилятора» для достижения, по крайней мере, той производительности, которую можно получить при ручной реализации asm. Мои знания об этом устарели, но, если не было значительного прогресса, реализации на основе встроенных функций во многих, если не в большинстве, случаях все еще заметно уступают. Конечно, их гораздо удобнее использовать, но я бы сказал, что программисты действительно не заботятся об этом, когда они хотят погрузиться в мир конкретных инструкций ЦП.

Еще одно интересное соображение заключается в том, что встроенные функции могут быть объединены с резервным кодом на архитектурах, где они не поддерживаются. Это дает вам лучшее из обоих миров: ваш код по-прежнему переносим - он может просто использовать некоторые операции с аппаратным ускорением, если оборудование их поддерживает. Конечно, это действительно окупается либо за очень распространенные инструкции, либо за то, что приложение имеет одну очевидную целевую архитектуру. Причина, по которой я упоминаю об этом, заключается в том, что хотя можно утверждать, что это потенциально может быть даже _ нежелательным_ с внутренними функциями, предоставленными _компилятором_ (поскольку вас, вероятно, заботит, действительно ли вы получаете ускоренные версии, плюс сложность компилятора никогда не бывает хорошей). Я бы сказал, что это совсем другая история, если встроенные функции предоставляются _library_ (и реализованы только с использованием встроенного asm). Фактически, это большая картина, которую я предпочел бы, хотя я вижу себя использующим встроенные функции больше, чем встроенные asm.

(Я считаю, что особенности RFC # 1199 несколько ортогональны этому обсуждению, поскольку они существуют в основном для того, чтобы заставить SIMD работать.)

@briansmith

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

Я не понимаю, что вы здесь имеете в виду. Это правда, что компилятор не может разбить asm на отдельные операции, чтобы уменьшить силу или оптимизировать на глазок. Но в модели GCC, по крайней мере, компилятор может выделять регистры, которые он использует, копировать их, когда он реплицирует пути кода, удалять их, если они никогда не использовались, и так далее. Если asm не является изменчивым, GCC имеет достаточно информации, чтобы обрабатывать его как любую другую непрозрачную операцию, например, fsin . Вся мотивация странного дизайна заключается в том, чтобы сделать встроенный asm чем-то, с чем оптимизатор может возиться.

Но я не очень часто им пользовался, особенно в последнее время. И у меня нет опыта использования этой функции в LLVM. Так что мне интересно, что изменилось или что я все это время неправильно понимал.

Мы обсуждали эту проблему на недавней рабочей неделе, поскольку в обзоре экосистемы no_std @japaric, макрос asm! является одной из наиболее часто используемых функций. К сожалению, мы не видели простого пути для стабилизации этой функции, но я хотел записать заметки, которые мы должны были сделать, чтобы не забыть обо всем этом.

  • Во-первых, в настоящее время у нас нет точной спецификации синтаксиса, принятого в макросе asm! . Прямо сейчас это обычно заканчивается «посмотрите на LLVM», который говорит «посмотрите на clang», который говорит «посмотрите на gcc», в котором нет хороших документов. В конце концов, это обычно сводится к «прочтите чей-нибудь пример и адаптируйте его» или «прочтите исходный код LLVM». Для стабилизации как минимум необходимо наличие спецификации синтаксиса и документации.

  • Насколько нам известно, сейчас LLVM не дает никаких гарантий стабильности. Макрос asm! - это прямая привязка к тому, что LLVM делает прямо сейчас. Означает ли это, что мы все еще можем свободно обновлять LLVM, когда захотим? Гарантирует ли LLVM, что никогда не нарушит этот синтаксис? Способом облегчить эту проблему может быть наш собственный уровень, который компилируется с синтаксисом LLVM. Таким образом, мы можем изменять LLVM, когда захотим, и если реализация встроенной сборки в LLVM изменится, мы можем просто обновить наш перевод на синтаксис LLVM. Если asm! хочет стать стабильным, нам в основном нужен какой-то механизм, гарантирующий стабильность в Rust.

  • Прямо сейчас существует довольно много ошибок, связанных со встроенной сборкой. Тег A-inline-assembly является хорошей отправной точкой, и в настоящее время он завален ICE, segfaults в LLVM и т. Д. В целом эта функция, реализованная сегодня, похоже, не соответствует гарантиям качества, которые другие ожидают от стабильной особенность в Rust.

  • Стабилизация встроенной сборки может очень затруднить реализацию альтернативного внутреннего интерфейса. Например, бэкэндам, таким как miri или cranelift, может потребоваться очень много времени для достижения паритета функций с бэкэндом LLVM, в зависимости от реализации. Это может означать, что здесь есть меньшая часть того, что можно сделать, но об этом важно помнить при рассмотрении вопроса о стабилизации встроенной сборки.


Несмотря на перечисленные выше проблемы, мы хотели быть уверены, что у нас появится хоть какая-то возможность продвинуть эту проблему вперед! С этой целью мы разработали несколько стратегий, позволяющих подтолкнуть встроенную сборку к стабилизации. Первичный путь вперед - это выяснить, что делает clang. Предположительно clang и C имеют эффективно стабильный синтаксис встроенного ассемблера, и вполне вероятно, что мы сможем просто отразить все, что делает clang (особенно относительно LLVM). Было бы здорово понять более подробно, как clang реализует встроенную сборку. Есть ли у clang собственный уровень перевода? Проверяет ли он какие-либо входные параметры? (так далее)

Еще одна возможность двигаться вперед - это посмотреть, есть ли ассемблер, который мы можем просто взять с полки где-нибудь еще, который уже стабилен. Некоторыми идеями здесь были nasm или ассемблер plan9. Использование ассемблера LLVM имеет те же проблемы с гарантиями стабильности, что и встроенная инструкция сборки в IR. (это возможно, но нам нужна гарантия стабильности перед его использованием)

Я хотел бы отметить, что встроенный asm-синтаксис LLVM отличается от синтаксиса, используемого clang / gcc. Различия включают:

  • LLVM использует $0 вместо %0 .
  • LLVM не поддерживает именованные asm-операнды %[name] .
  • LLVM поддерживает различные типы ограничений регистров: например, "{eax}" вместо "a" на x86.
  • LLVM поддерживает явные ограничения регистров ( "{r11}" ). В C вместо этого вы должны использовать регистровые asm-переменные для привязки значения к регистру ( register asm("r11") int x ).
  • Ограничения LLVM "m" и "=m" в основном нарушены. Clang переводит их в косвенные ограничения памяти "*m" и "=*m" и передает адрес переменной в LLVM вместо самой переменной.
  • так далее...

Clang преобразует встроенный asm из формата gcc в формат LLVM перед передачей его в LLVM. Он также выполняет некоторую проверку ограничений: например, он гарантирует, что операнды "i" являются константами времени компиляции,


В свете этого я думаю, что мы должны реализовать тот же перевод и проверку, что и clang, и поддерживать правильный inline asm синтаксис gcc вместо странного LLVM.

В Интернете есть отличное видео о сводках с D, MSVC, gcc, LLVM и Rust со слайдами.

Как человек, который хотел бы иметь возможность использовать встроенный ASM в стабильном Rust, и с большим опытом, чем я хочу, пытаясь получить доступ к некоторым API-интерфейсам LLVM MC из Rust, некоторые мысли:

  • Встроенный ASM - это, по сути, копирование и вставка фрагмента кода в выходной файл .s для сборки после некоторой подстановки строки. Он также имеет присоединения входных и выходных регистров, а также закрытые регистры. Этот базовый фреймворк вряд ли когда-либо действительно изменится в LLVM (хотя некоторые детали могут немного отличаться), и я подозреваю, что это довольно независимое от фреймворка представление.

  • Создать перевод из спецификации, ориентированной на Rust, в формат IR, ориентированный на LLVM, несложно. И это может быть целесообразно - синтаксис rust {} для форматирования не влияет на язык ассемблера, в отличие от нотации LLVM $ и GCC % .

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

  • Попытка разобрать сборку самостоятельно, скорее всего, станет кошмаром. API LLVM-C не раскрывает логику MCAsmParser, и эти классы довольно раздражают при работе с bindgen (я сделал это).

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

Я немного поигрался, чтобы увидеть, что можно сделать с процедурными макросами. Я написал тот, который преобразует встроенную сборку в стиле GCC в стиль ржавчины https://github.com/parched/gcc-asm-rs. Я также начал работать над тем, который использует DSL, где пользователю не нужно понимать ограничения, и все они обрабатываются автоматически.

Итак, я пришел к выводу, что я думаю, что ржавчина должна просто стабилизировать голые строительные блоки, а затем сообщество может выполнять итерацию вне дерева с помощью макросов, чтобы найти лучшие решения. По сути, просто стабилизируйте стиль llvm, который у нас есть сейчас, только с ограничениями «r» и «i» и, возможно, «m», и без клобберов. Другие ограничения и клобберы могут быть стабилизированы позже с их собственными вещами типа mini rfc.

Лично я начинаю чувствовать, что стабилизация этой функции - это крупномасштабная задача, которая никогда не будет выполнена, если каким-то образом кто-то не наймет штатного специалиста-подрядчика, чтобы продвигать ее в течение целого года. Я хочу верить, что предложение @parched о asm! сделает это послушным. Я надеюсь, что кто-то подберет его и побежит с ним. Но если это не так, то нам нужно перестать пытаться найти удовлетворительное решение, которое никогда не придет, и перейти к неудовлетворительному решению, которое: стабилизирует asm! как есть, бородавки, ICE, ошибки и все такое. , с яркими жирными предупреждениями в документах, рекламирующих мусор и непереносимость, и с намерением осудить когда-нибудь, если удовлетворительная реализация когда-либо чудесным образом снизойдет, посланным Богом, на свое небесное воинство. IOW, мы должны сделать именно то macro_rules! (и, конечно же, как и в случае с macro_rules! , у нас может быть короткий период безумного ограничения и надежной защиты от утечек в будущем). Мне грустно разводить альтернативные бэкенды, но для системного языка стыдно относить встроенную сборку в такое состояние, и мы не можем допустить, чтобы гипотетическая возможность наличия нескольких бэкэндов продолжала препятствовать существованию одного реально используемого бэкэнда. Прошу вас доказать, что я неправ!

стыдно для системного языка сводить встроенную сборку в такое состояние

Что касается данных, я сейчас работаю над ящиком, который зависит от gcc с единственной целью - сгенерировать некоторый asm со стабильным Rust: https://github.com/main--/unwind- rs / blob / 266e0f26b6423f4a2b8a8c72442b319b5c33b658 / src / unwind_helper.c


Хотя у этого, безусловно, есть свои преимущества, я немного опасаюсь «стабилизировать строительные блоки, а все остальное оставить на усмотрение макрокоманд». По сути, он передает проектирование, RFC и процесс реализации на аутсорсинг тем, кто хочет выполнить эту работу, потенциально никому. Конечно, все дело в более слабых гарантиях стабильности / качества (компромисс в том, что иметь что-то несовершенное уже намного лучше, чем вообще ничего не иметь), я это понимаю.

По крайней мере, строительные блоки должны быть хорошо спроектированы - а, на мой взгляд, "expr" : foo : bar : baz определенно не так. Я не припомню, чтобы когда-либо получал правильный заказ с первой попытки, мне всегда нужно его искать. «Магические категории, разделенные двоеточиями, в которых вы указываете постоянные строки с магическими символами, которые в конечном итоге делают волшебные вещи с именами переменных, которые вы также каким-то образом просто смешиваете» - это просто плохо.

Одна идея,…

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

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

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

Даже без dynasm это также можно использовать как способ создания макросов для инструкций cpuid / rtdsc, которые будут просто транслироваться в необработанную последовательность байтов.

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

[РЕДАКТИРОВАТЬ: я не думаю, что все, что я сказал в этом комментарии, является правильным.]

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

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

Я думаю, было бы странно стабилизировать формат LLVM, когда даже Clang этого не делает. Предположительно, он использует внутреннюю поддержку LLVM, но представляет собой интерфейс, больше похожий на GCC.

Я на 100% согласен с тем, что «Rust поддерживает именно то, что поддерживает Clang», и прекращаю это дело, особенно с учетом того, что позиция AFAIK Clang такова: «Clang поддерживает именно то, что поддерживает GCC». Если у нас когда-либо будет настоящая спецификация Rust, мы можем смягчить язык до «встроенная сборка определяется реализацией». Приоритет и фактическая стандартизация - мощные инструменты. Если мы сможем перепрофилировать собственный код Clang для перевода синтаксиса GCC в LLVM, тем лучше. Проблемы с альтернативным бэкэндом никуда не делись, но теоретически фронтенд Rust для GCC не будет сильно затруднен. Меньше нам нужно проектировать, меньше нам бесконечно ездить на велосипеде, меньше учить, меньше поддерживать.

Если мы стабилизируем что-то, определенное в терминах того, что поддерживает clang, мы должны назвать это clang_asm! . Имя asm! должно быть зарезервировано для чего-то, что было разработано с помощью полного процесса RFC, как и другие основные функции Rust. #bikeshed

Вот несколько вещей, которые я хотел бы видеть во встроенной сборке Rust:

  • Шаблон с заменами уродлив. Я всегда прыгаю между текстом сборки и списком ограничений. Краткость побуждает людей использовать позиционные параметры, что ухудшает читаемость. Символические имена часто означают, что одно и то же имя повторяется три раза: в шаблоне, называя операнд, и в выражении, связанном с операндом. Слайды, упомянутые в комментарии Алекса, показывают, что D и MSVC позволяют вам просто ссылаться на переменные в коде, что кажется намного лучше.

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

Норман Рэмси и Мэри Фернандес написали несколько статей о наборе инструментов машинного кода в

Я хотел бы еще раз повторить выводы последней рабочей недели :

  • Насколько нам известно, сегодня документации по этой функции практически нет. Это включает внутреннее устройство LLVM и все такое.
  • Насколько нам известно, LLVM не дает никаких гарантий стабильности. Насколько нам известно, реализация встроенной сборки в LLVM может измениться в любой день.
  • В настоящее время это очень глючная функция в rustc. Он переполнен (во время компиляции) ошибками сегментов, ICE и странными ошибками LLVM.
  • Без спецификации для этого практически невозможно даже представить себе альтернативный бэкэнд.

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

По крайней мере, я твердо уверен, что пуля (2) не может быть скомпрометирована (так называемое определение стабильного в «стабильном канале»). Было бы довольно грустно отказаться от других пунктов, поскольку это подрывает ожидаемое качество компилятора Rust, которое в настоящее время довольно высокое.

@jcranmer написал:

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

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

@alexcrichton написал:

Насколько нам известно, LLVM не дает никаких гарантий стабильности. Насколько нам известно, реализация встроенной сборки в LLVM может измениться в любой день.

Документы LLVM гарантируют: «Новые выпуски могут игнорировать функции из старых выпусков, но не могут их неправильно скомпилировать». (относительно ИК-совместимости). Это скорее ограничивает, насколько они могут изменять встроенную сборку, и, как я утверждал выше, на самом деле нет какой-либо жизнеспособной замены на уровне LLVM, которая радикально изменила бы семантику по сравнению с текущей ситуацией (в отличие, скажем, от текущих проблем, связанных с ядом и undef). Поэтому утверждение, что его предполагаемая нестабильность не позволяет использовать его в качестве основы для блока Rust asm! является в некоторой степени нечестным. Это не значит, что с ним связаны другие проблемы (плохая документация, хотя она и улучшилась; отстойность ограничений; плохая диагностика; и ошибки в менее распространенных сценариях - вот те, что приходят на ум).

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

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

Я думаю, было бы странно стабилизировать формат LLVM, когда даже Clang этого не делает.

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

По крайней мере, строительные блоки должны быть хорошо спроектированы - а, на мой взгляд, "expr" : foo : bar : baz определенно не так.

Согласовано. По крайней мере, я предпочитаю необработанный формат LLVM, в котором все ограничения и сбои находятся в одном списке. В настоящее время существует необходимость указать префикс «=» и поместить его в список вывода. Я также думаю, что LLVM рассматривает это больше как вызов функции, где выходы являются результатом выражения, AFAIK текущая реализация asm! - единственная часть ржавчины, которая имеет параметры "out".

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

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

Если мы хотим создать внешний ассемблер

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

Что касается критических изменений во встроенном ассемблере LLVM, мы находимся в одной лодке с Clang. То есть, если они вносят какие-то изменения, нам просто нужно их обойти, когда они произойдут.

Если мы стабилизируем что-то, определенное в терминах того, что поддерживает clang, мы должны назвать это clang_asm !. Асм! name должно быть зарезервировано для чего-то, что было разработано с помощью полного RFC-процесса, как и другие основные функции Rust. #bikeshed

Я только за. +1

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

@parched Следуя предложению asm! , с радостью сможет использовать его.

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

Если синтаксис сборки GCC действительно не указан и не задокументирован по прошествии 30 лет, то можно с уверенностью предположить, что создание задокументированного подъязыка сборки либо настолько сложная задача, что Rust выходит за рамки возможностей ее выполнения с учетом наших ограниченных ресурсов, либо что Людям, которые хотят использовать сборку, просто плевать.

Насколько нам известно, LLVM не дает никаких гарантий стабильности. Насколько нам известно, реализация встроенной сборки в LLVM может измениться в любой день.

Кажется маловероятным, что реализация встроенной сборки GCC / Clang когда-либо изменится, поскольку это нарушит весь код C, написанный с 90-х годов.

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

Рискуя оказаться бессердечным, перспектива альтернативных бэкендов будет спорной, если Rust как язык не выживет из-за его досадной неспособности перейти на ассемблер. Nightly недостаточно, если только кто-то не хочет молчаливо поддерживать идею о том, что Nightly - это Rust, который больше подрывает гарантии стабильности Rust, чем перспектива изменений LLVM.

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

Я не лгу, когда говорю, что каждый день я благодарен разработчикам на Rust за их отношение и огромный стандарт качества, которого они придерживаются (на самом деле, иногда мне хочется, чтобы вы все замедлили, чтобы вы могли поддерживать это качество. не выжигая себя, как это сделал Брайан). Однако, говоря от лица человека, который был здесь, когда luqmana добавил макрос Rust понадобится стабильная встроенная сборка где-то в 2018 году. Для этого нам нужен предшествующий уровень техники. Ситуация macro_rules! подтвердила, что иногда хуже - лучше. Еще раз прошу кого-нибудь доказать, что я неправ.

FWIW и опоздание на вечеринку. Мне нравится, что предлагает @florob в разговоре с

// Add 5 to variable:
let mut var = 0;
unsafe {
    asm!("add $5, {}", inout(reg) var);
}

// Get L1 cache size
let ebx: i32;
let ecx: i32;
unsafe {
    asm!(r"
        mov $$4, %eax;
        xor %ecx, %ecx;
        cpuid;
        mov %ebx, {};",
        out(reg) ebx, out(ecx) ecx, clobber(eax, ebx, edx)
    );
}
println!("L1 Cache: {}", ((ebx >> 22) + 1)
    * (((ebx >> 12) & 0x3ff) + 1)
    * ((ebx & 0xfff) + 1) * (ecx + 1));

Как насчет следующей стратегии: переименовать текущий asm в llvm_asm (плюс, возможно, некоторые незначительные изменения) и указать, что его поведение является деталью реализации LLVM, поэтому гарантия стабильности Rust не распространяется на него полностью? Проблема разных бэкэндов должна быть более или менее решена с помощью target_feature подобной функциональности для условной компиляции в зависимости от используемого бэкэнда. Да, такой подход немного помешает стабильности Rust, но держать сборку в подвешенном состоянии, как это, по-своему вредно для Rust.

Я разместил предварительный RFC с предложением альтернативного синтаксиса на внутреннем форуме: https://internals.rust-lang.org/t/pre-rfc-inline-assembly/6443 . Обратная связь приветствуется.

Мне кажется, что здесь лучшее - определенно враг хорошего тона. Я полностью поддерживаю включение макроса gcc_asm! или clang_asm! или llvm_asm! (или любого его подходящего подмножества) в стабильную версию на данный момент с совместимым синтаксисом и семантикой, пока не будет разработано лучшее решение . Я не считаю, что поддерживать такую ​​вещь вечно, как огромное бремя обслуживания: более сложные системы, предложенные выше, выглядят так, как будто они довольно легко поддержат простое превращение макросов старого стиля в синтаксический сахарин для нового.

У меня есть двоичная программа http: //[email protected]/BartMassey/popcount, которая требует встроенной сборки для инструкции x86_64 popcntl . Эта встроенная сборка - единственное, что хранит этот код каждую ночь. Код был получен из программы C. 12-летней давности.

Прямо сейчас моя сборка зависит от

    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]

а затем получает информацию о cpuid чтобы узнать, присутствует ли popcnt . Было бы неплохо иметь в Rust что-то похожее на недавнюю библиотеку Google cpu_features https://opensource.googleblog.com/2018/02/cpu-features-library.html в Rust, но c'est la vie.

Поскольку это демонстрационная программа, я бы хотел сохранить встроенную сборку. Для реальных программ внутренней функции count_ones() было бы достаточно - за исключением того, что для ее использования popcntl требуется передача "-C target-cpu = native" в Cargo, возможно, через RUSTFLAGS (см. проблему № 1137 и несколько связанных проблем), поскольку распространение .cargo/config с моим исходным кодом не кажется хорошей идеей, а это означает, что прямо сейчас у меня есть Makefile, вызывающий Cargo.

Короче говоря, было бы неплохо, если бы можно было использовать причудливую инструкцию popcount от Intel и других в реальных приложениях, но это кажется сложнее, чем должно быть. Внутренние функции - это не совсем ответ. Текущий asm! - нормальный ответ, если бы он был доступен в стабильной версии. Было бы здорово иметь лучший синтаксис и семантику для встроенной сборки, но мне это действительно не нужно. Было бы здорово указать target-cpu=native прямо в Cargo.toml , но это не решило бы мою проблему.

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

@BartMassey Я не понимаю, зачем тебе так отчаянно нужно компилировать в popcnt? Единственная причина, по которой я вижу, - это производительность, и ИМО, вам обязательно нужно просто использовать count_ones () в этом случае. Вам нужен не встроенный asm, а target_feature (rust-lang / rfcs # 2045), чтобы вы могли сообщить компилятору, что ему разрешено выдавать popcnt.

@BartMassey, вам даже не нужно использовать для этого встроенную сборку, просто используйте coresimd cfg_feature_enabled!("popcnt") чтобы узнать, поддерживает ли процессор, на котором работает ваш двоичный файл, инструкцию popcnt (это будет разрешите это во время компиляции, если это возможно).

coresimd также предоставляет встроенную функцию popcnt которая гарантированно использует инструкцию popcnt .

@gnzlbg

coresimd также предоставляет встроенную функцию popcnt, которая гарантированно использует инструкцию popcnt.

Это немного не по теме, но это утверждение не совсем верно. _popcnt64 использует leading_zeros под капотом, поэтому, если функция popcnt не будет включена пользователем ящика, а автор ящика забудет использовать #![cfg(target_feature = "popcnt")] эта встроенная функция получит скомпилирован в неэффективную сборку и нет никаких гарантий от этого.

таким образом, если функция popcnt не будет включена пользователем ящика

Это неверно , так как внутренние использования #[target_feature(enable = "popcnt")] атрибута , чтобы включить popcnt функции для внутреннего безоговорочно, независимо от того , что пользователь клети включает или отключает. Кроме того, атрибут assert_instr(popcnt) гарантирует, что внутренний дизассемблируется в popcnt на всех x86 платформах, поддерживаемых Rust.

Если кто-то использует Rust на платформе x86 которую Rust в настоящее время не поддерживает, тогда дело за тем, кто переносит core чтобы убедиться, что эти встроенные функции генерируют popcnt на этой цели.


РЕДАКТИРОВАТЬ: @newpavlov

таким образом, если функция popcnt не будет включена пользователем ящика, а автор ящика забудет использовать #! [cfg (target_feature = "popcnt")], эта встроенная функция будет скомпилирована в неэффективную сборку, и от нее нет никаких гарантий.

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

Прежде всего, извинения за срыв обсуждения. Просто хотел повторить свою основную мысль: «Я полностью поддерживаю включение макроса gcc_asm! Или clang_asm! Или llvm_asm! (Или любого его подходящего подмножества) в стабильную версию с совместимым синтаксисом и семантикой на данный момент, пока не будет разработано лучшее решение. "

Суть встроенной сборки в том, что это тест / демонстрация popcount. Мне нужна настоящая гарантированная инструкция popcntl когда это возможно, как в качестве базовой линии, так и для иллюстрации того, как использовать встроенную сборку. Я также хочу гарантировать, что count_ones() использует инструкцию popcount, чтобы Rustc не выглядел ужасно по сравнению с GCC и Clang.

Спасибо, что указали на target_feature=popcnt . Я подумаю, как его тут использовать. Я думаю, что хочу протестировать count_ones() независимо от того, для какого процессора компилируется пользователь, и независимо от того, есть ли у него инструкция popcount. Я просто хочу убедиться, что если у целевого процессора есть popcount count_ones() использует его.

Ящики stdsimd / coresimd выглядят хорошо, и, вероятно, их следует включить для этих тестов. Спасибо! Для этого приложения я бы предпочел использовать как можно меньше функций, выходящих за рамки стандартного языка (я уже чувствую себя виноватым из-за lazy_static ). Однако эти объекты выглядят слишком хорошо, чтобы их игнорировать, и похоже, что они уже на пути к тому, чтобы стать «официальными».

В @nbp есть

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

cc @eddyb

@nagisa - это немного больше, чем просто

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

Стандартизация байтовых последовательностей, вероятно, является самым простым способом продвижения вперед путем переноса ассемблера в driver / proc-macro.

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

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

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

В чем-то вроде этого я позволяю компилятору выделять регистры, ожидая, что он предоставит мне то, что наиболее удобно и эффективно. Например, на x86 64, если five_time_x является возвращаемым значением, тогда компилятор может выделить eax и если x является параметром функции, он может уже быть доступен в каком-то регистре. Конечно, компилятор точно знает только, как он будет выделять регистры довольно поздно в последовательности компиляции (особенно если это не так тривиально, как просто параметры функций и возвращаемые значения).

Будет ли ваше предлагаемое решение работать с чем-то подобным?

@nbp Я должен сказать, что меня немного смутило это предложение.
Во-первых, стандартизация языка ассемблера никогда не была тем, чего мы хотели достичь с помощью встроенной сборки. По крайней мере, для меня предпосылка всегда заключалась в том, что язык ассемблера, используемый системным ассемблером, будет принят.
Проблема не в том, чтобы сборка была проанализирована / собрана, мы можем легко передать это LLVM.
Проблема заключается в заполнении шаблонной сборки (или предоставлении LLVM необходимой информации для этого) и указании входов, выходов и затирающих устройств.
Последующая проблема фактически не решена вашим предложением. Однако это смягчается, потому что вы не будете / не сможете поддерживать классы регистров (о которых спрашивает @simias ), а только конкретные регистры.
В точке, где ограничения упрощены до такой степени, на самом деле так же легко поддерживать «настоящую» встроенную сборку. Первый аргумент - это строка, содержащая (не шаблонную) сборку, остальные аргументы - это ограничения. Это довольно легко сопоставить с выражениями встроенного ассемблера LLVM.
С другой стороны, вставка необработанных байтов, насколько я знаю (или могу сказать из Справочного руководства LLVM IR), не поддерживается LLVM. Таким образом, мы в основном расширяем LLVM IR и повторно реализуем функцию (сборку системной сборки), которая уже присутствует в LLVM, используя отдельные ящики.

@nbp

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

Итак, как это сделать? У меня есть последовательность байтов с жестко закодированными регистрами, что в основном означает, что регистры ввода / вывода, клобберы и т. Д. Жестко запрограммированы внутри этой последовательности байтов.

Теперь я вставляю эти байты где-нибудь в свой двоичный файл ржавчины. Как мне сообщить rustc, какие регистры являются входными / выходными, какие регистры были заторможены и т. Д.? Как решить эту меньшую проблему, чем стабилизация встроенной сборки? Мне кажется, что это именно то, что делает встроенная сборка, только, может быть, немного сложнее, потому что теперь нужно дважды указывать Clobbers ввода / вывода в написанной сборке и каким бы способом мы ни передавали эту информацию в rustc. Кроме того, rustc будет нелегко проверить это, потому что для этого он должен иметь возможность анализировать последовательность байтов в сборку, а затем проверять ее. Что мне не хватает?

@simias

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

Это было бы невозможно, поскольку необработанные байты не допускают альфа-переименование регистров, и регистры должны быть принудительно реализованы с помощью последовательности кода впереди.

@Florob

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

Насколько я понимаю, полагаться на системный ассемблер - это не то, на что мы хотим полагаться, это скорее принятый недостаток как часть asm! макрос. Также полагаюсь на asm! использование синтаксиса LLVM было бы болезненным для разработки дополнительных серверных приложений.

@gnzlbg

Итак, как это сделать? У меня есть последовательность байтов с жестко закодированными регистрами, что в основном означает, что регистры ввода / вывода, клобберы и т. Д. Жестко запрограммированы внутри этой последовательности байтов.

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

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_raw!{
       bytes: [0x91],
       inputs: [(std::asm::eax, a), (std::asm::ecx, b)],
       clobbered: [],
       outputs: (std::asm::eax, std::asm::ecx),
    }
  }
}

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

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_x64!{
       ; <-- (eax, a), (ecx, b)
       xchg eax, ecx
       ; --> (eax, ecx)
    }
  }
}

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

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

@nbp

Насколько я понимаю, полагаться на системный ассемблер - это не то, на что мы хотим полагаться, это скорее принятый недостаток как часть asm! макрос. Также полагаюсь на asm! использование синтаксиса LLVM было бы болезненным для разработки дополнительных серверных приложений.

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

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

Я согласен с тем, что синтаксис LLVM (или gcc) для этого дерьма, но переход к предварительно скомпилированным байтам означает, что любой ящик asm теперь должен установить полный ассемблер и, возможно, полный распределитель регистров (или заставить программистов вручную выделять регистры) или попытаться использовать системный ассемблер. На тот момент это не похоже на то, чтобы действительно добавить большую ценность.

@jcranmer

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

https://github.com/CensoredUsername/dynasm-rs

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

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

Если большой двоичный объект сборки плохо интегрируется с окружающей сборкой, созданной компилятором, я мог бы просто добавить заглушку ASM во внешний метод C-стиля в автономном файле сборки .s, поскольку вызовы функций имеют регистр того же типа. -ограничения размещения. Это уже работает сегодня, хотя я полагаю, что его встроенная в rustc может упростить систему сборки по сравнению с наличием отдельного файла сборки. Я предполагаю, что я хочу сказать, что ваше предложение, ИМО, не уведет нас очень далеко по сравнению с нынешней ситуацией.

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

@jcranmer

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

Я не уверен, что понимаю, что вы имеете в виду, очевидно, что синтаксис ASM не переносится между архитектурами. И даже в рамках одной и той же архитектуры часто существуют варианты и опции, которые изменяют способ сборки языка.

Я могу привести MIPS в качестве примера, есть два важных флага конфигурации, которые настраивают поведение ассемблера: at и reorder . at говорит, разрешено ли ассемблеру неявно использовать регистр AT (временный ассемблер) при сборке определенных псевдо-инструкций. Код, который явно использует AT для хранения данных, должен быть собран с at иначе он сломается.

reorder определяет, обрабатывает ли кодировщик вручную слоты задержки ветвления или доверяет ли ассемблер разбираться с ними. Сборка кода с неправильной настройкой reorder почти наверняка сгенерирует поддельный машинный код. Когда вы пишете сборку MIPS, вы должны всегда знать текущий режим, если он содержит какую-либо инструкцию ветвления. Например, невозможно узнать значение этого списка MIPS, если вы не знаете, включен ли reorder :

    addui   $a0, 4
    jal     some_func
    addui   $a1, $s0, 3

32-битная сборка ARM имеет варианты Thumb / ARM, важно знать, на какой набор инструкций вы нацеливаетесь (и вы можете изменять его на лету при вызове функций). Смешивать оба набора нужно очень осторожно. Код ARM также обычно загружает большие немедленные значения с использованием неявной нагрузки, связанной с ПК, если вы предварительно соберете свой код, вам нужно будет быть осторожным с тем, как вы передаете эти значения, поскольку они должны оставаться рядом, но не являются фактическими инструкциями с четко очерченное место. Я говорю о псевдо-инструкциях вроде:

   ldr   r2, =0x89abcdef

MIPS, с другой стороны, имеет тенденцию разделять непосредственное значение на два 16-битных значения и использовать комбинацию lui / ori или lui / andi. Обычно он скрывается за псевдо-инструкциями li / la но если вы пишете код с noreorder и не хотите тратить слот задержки, иногда вам приходится обрабатывать это вручную, что приводит к забавно выглядящему коду:

.set noreorder

   /* Display a message using printf */
   lui $a0, %hi(hello)
   jal printf
   ori $a0, %lo(hello)

.data

hello:
.string "Hello, world!\n"

Конструкции %hi и %lo - это способ сообщить сборке о необходимости создания ссылки на старшие и младшие 16 битов символа hello соответственно.

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

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

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

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

Привет, народ,

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

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

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

FWIW Rust здесь не такой уж особенный, MSVC также не имеет встроенного asm для x86_64. У них действительно есть очень странная реализация, в которой вы можете использовать переменные в качестве операндов, но это работает только для x86.

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

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

Кроме того, большинство вещей можно сделать с помощью открытых встроенных функций платформы. x86 и x86_64 были стабилизированы, а другие платформы находятся в стадии разработки. Большинство людей ожидают, что они достигнут 95-99% целей. Вы можете увидеть мой собственный crate jetscii в качестве примера использования некоторых встроенных функций.

Мы только что объединили jemalloc PR, который использует встроенную сборку для обхода ошибок генерации кода в LLVM - https://github.com/jemalloc/jemalloc/pull/1303 . Кто-то использовал встроенную сборку в этой проблеме (https://github.com/rust-lang/rust/issues/53232#issue-349262078), чтобы обойти ошибку генерации кода в Rust (LLVM), которая произошла в ящике jetscii. Оба произошли за последние две недели, и в обоих случаях пользователи пытались использовать встроенные функции, но компилятор не смог.

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

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

@eddyb Ну, я пишу небольшую библиотеку матричной алгебры. Внутри этой библиотеки я реализую BLAS, возможно, некоторые процедуры LAPACK (еще не существующие) в Rust, потому что я хотел, чтобы библиотека была чистой реализацией ржавчины. Пока в этом нет ничего серьезного, но в любом случае я хотел, чтобы пользователь мог выбрать некоторую скорость asm и веселье, особенно с помощью операции GEMM, которая раньше была важна (во всяком случае, наиболее часто используемой, и если вы будете следовать подходу людей BLIS это все, что вам нужно), по крайней мере, в x86 / x86_64. И это полная история. Очевидно, я тоже могу использовать ночной канал, я просто хотел немного продвинуться в прагматическом направлении стабилизации функции.

@shepmaster Существует множество вариантов использования, для которых встроенных функций недостаточно. В верхней части моей головы из недавних материалов, где я подумал: «Почему, почему у Rust нет стабильного asm?», Нет никаких встроенных функций XACQUIRE / XRELEASE.

Стабильный встроенный asm имеет решающее значение, и нет, встроенных функций недостаточно.

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

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

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

Большинство людей ожидают, что [внутренние функции] достигнут 95-99% целей .

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

Пока в этом нет ничего серьезного, но в любом случае я хотел, чтобы пользователь мог выбрать некоторую скорость asm и веселье, особенно с помощью операции GEMM, которая раньше была важна (во всяком случае, наиболее часто используемой, и если вы будете следовать подходу людей BLIS это все, что вам нужно), по крайней мере, в x86 / x86_64.

Я до сих пор не понимаю, к каким инструкциям вам нужно получить доступ, чего нельзя добиться без встроенной сборки. Или это просто конкретная последовательность арифметических инструкций?
Если да, то сравнивали ли вы эквивалентный исходный код Rust со встроенной сборкой?

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

Что ж, когда вы говорите о сборке в математике, вы в основном говорите об использовании регистров и инструкций SIMD, таких как _mm256_mul_pd, _mm256_permute2f128_pd и т. Д., И об операциях векторизации там, где они выполняются. Дело в том, что вы можете использовать разные подходы к векторизации, и обычно это небольшой метод проб и ошибок, пока вы не получите оптимизированную производительность для процессора, на который вы нацеливаетесь, и того использования, которое вы имеете в виду. Поэтому обычно на уровне библиотеки вам сначала нужно запросить процессор, вводящий код asm, чтобы узнать набор поддерживаемых инструкций и регистров, а затем выполнить условную компиляцию конкретной версии вашего математического ядра asm.

Если да, то сравнивали ли вы эквивалентный исходный код Rust со встроенной сборкой?

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

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

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

Мы могли бы определять функции на ассемблере и вызывать их через FFI, связывая их на отдельном шаге, но я не знаю ни одного серьезного проекта с нуля, который бы делал это исключительно, поскольку он имеет недостатки как с точки зрения сложности, так и с точки зрения производительности. Redox использует asm!. Обычные подозреваемые в Linux, BSD, macOS, Windows и т. Д. - все широко используют встроенный ассемблер. Циркон и seL4 делают это. Даже Plan 9 обрушился на это несколько лет назад на вилке Харви.

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

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

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

Последний предварительный RFC (https://internals.rust-lang.org/t/pre-rfc-inline-assembly/6443) достиг консенсуса 6 месяцев назад (по крайней мере, по большинству фундаментальных вопросов), поэтому следующий шаг заключается в том, чтобы представить RFC, основанный на этом. Если вы хотите, чтобы это произошло быстрее, я рекомендую связаться с @Florob по этому поводу.

Как бы то ни было, мне нужен прямой доступ к регистрам FSGS, чтобы получить указатель на структуру TEB в Windows, мне также нужен _bittest64 -подобная встроенная функция для применения bt к произвольной области памяти, ни один из них я не мог найти способ обойтись без встроенной сборки или внешних вызовов.

Третий момент, упомянутый здесь, беспокоит меня, так как LLVM действительно предпочитает Just Crash, если что-то не так, обеспечивая отсутствие сообщений об ошибках, что бы то ни было.

@MSxDOS

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

Нетрудно добавить это к stdsimd , clang реализует их с помощью встроенной сборки (https://github.com/llvm-mirror/clang/blob/c1c07cca8cae5f924cedaac7b202b0f3c167111d/test/CodeGen/bittest-intrin .c # L45), но мы можем использовать это в библиотеке std и предоставить внутреннюю функцию безопасному Rust.

Поощряйте желание открыть в репозитории stdsimd вопрос об отсутствующих встроенных функциях.

@josevalaad

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

Ах, я подозревал, что это может быть так. Что ж, если вы хотите попробовать, вы можете преобразовать сборку в внутренние вызовы std::arch и посмотреть, получите ли вы от этого такую ​​же производительность.

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

@dancrossnyc Если вы не возражаете, я спрашиваю, есть ли в вашей ситуации какие-либо варианты использования / особенности платформы, которые требуют встроенной сборки?

@MSxDOS Может стоит выставить встроенные функции для чтения "сегментных" регистров?


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

Может быть, нам стоит провести сбор данных и выяснить, что люди действительно хотят от asm!

Я хочу asm! за:

  • работа с встроенными функциями, не предоставленными компилятором
  • работа над ошибками компилятора / генерация неоптимального кода
  • выполнение операций, которые не могут быть выполнены с помощью последовательности одиночных внутренних вызовов, например, EFLAGS чтения-изменения-записи EFLAGS, где LLVM разрешено изменять eflags между чтением и записью, и где LLVM также предполагает, что пользователь не будет изменять это за его спиной (то есть, единственный способ безопасной работы с EFLAGS - это записать операции чтения-изменения-записи в виде одного атомарного блока asm! ).

и посмотрите, сколько из них можно поддержать каким-либо другим способом.

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

Скопировано из моего сообщения в ветке до RFC, вот некоторая встроенная сборка (ARM64), которую я использую в своем текущем проекте:

// Common code for interruptible syscalls
macro_rules! asm_interruptible_syscall {
    () => {
        r#"
            # If a signal interrupts us between 0 and 1, the signal handler
            # will rewind the PC back to 0 so that the interrupt flag check is
            # atomic.
            0:
                ldrb ${0:w}, $2
                cbnz ${0:w}, 2f
            1:
               svc #0
            2:

            # Record the range of instructions which should be atomic.
            .section interrupt_restart_list, "aw"
            .quad 0b
            .quad 1b
            .previous
        "#
    };
}

// There are other versions of this function with different numbers of
// arguments, however they all share the same asm code above.
#[inline]
pub unsafe fn interruptible_syscall3(
    interrupt_flag: &AtomicBool,
    nr: usize,
    arg0: usize,
    arg1: usize,
    arg2: usize,
) -> Interruptible<usize> {
    let result;
    let interrupted: u64;
    asm!(
        asm_interruptible_syscall!()
        : "=&r" (interrupted)
          "={x0}" (result)
        : "*m" (interrupt_flag)
          "{x8}" (nr as u64)
          "{x0}" (arg0 as u64)
          "{x1}" (arg1 as u64)
          "{x2}" (arg2 as u64)
        : "x8", "memory"
        : "volatile"
    );
    if interrupted == 0 {
        Ok(result)
    } else {
        Err(Interrupted)
    }
}

@Amanieu обратите внимание, что @japaric работает над встроенными функциями для ARM . Стоит проверить, отвечает ли это предложение вашим потребностям.

@shepmaster

@Amanieu обратите внимание, что @japaric работает над встроенными

Стоит отметить, что:

  • эта работа не заменяет встроенную сборку, а лишь дополняет ее. Этот подход реализует API поставщиков в std::arch , этих API уже недостаточно для некоторых людей.

  • этот подход применим только тогда, когда последовательность внутренних вызовов, таких как foo(); bar(); baz(); создает код, неотличимый от этой последовательности инструкций - это не обязательно так, и когда это не так, код, который выглядит правильным, дает в лучшем случае неправильный результаты, а в худшем случае имеет неопределенное поведение (у нас уже были ошибки из-за этого в x86 и x86_64 в std , например, https://github.com/rust- lang-питомник / stdsimd / blob / master / coresimd / x86 / cpuid.rs # L108 - другие архитектуры также имеют эти проблемы).

  • у некоторых встроенных функций есть аргументы немедленного режима, которые нельзя передать через вызов функции, так что foo(3) не будет работать. Каждое решение этой проблемы в настоящее время представляет собой хитрый обходной путь, а в некоторых случаях обходные пути в Rust в настоящее время невозможны, поэтому мы просто не предоставляем некоторые из этих встроенных функций.

Поэтому, если API-интерфейсы поставщика реализованы в Rust, доступны на std::arch и могут быть объединены для решения проблемы, я согласен, что они лучше, чем встроенная сборка. Но то и дело API-интерфейсы недоступны, может быть, даже нереализуемы, и / или они не могут быть правильно скомбинированы. Хотя мы могли бы исправить «проблемы с реализуемостью» в будущем, если то, что вы хотите сделать, не предоставляется API поставщика или API-интерфейсы не могут быть объединены, этот подход вам не поможет.

Что может быть очень удивительным в реализации встроенных функций LLVM (особенно SIMD), так это то, что они вообще не соответствуют явному отображению встроенных функций в инструкции Intel - они подвергаются широкому спектру оптимизаций компилятора. Например, я помню один раз, когда я попытался уменьшить нагрузку на память, вычислив некоторые константы из других констант, вместо того, чтобы загружать их из памяти. Но LLVM просто продолжал постоянно сворачивать все это обратно в точную нагрузку на память, которую я пытался избежать. В другом случае я хотел исследовать замену 16-битного перемешивания на 8-битное перемешивание, чтобы уменьшить давление на порт 5. Тем не менее, в своей бесконечной мудрости всегда полезный оптимизатор LLVM заметил, что мое 8-битное перемешивание на самом деле является 16-битным перемешиванием, и заменил его.

Обе оптимизации, безусловно, обеспечивают лучшую пропускную способность (особенно в условиях гиперпоточности), но не сокращение задержки, которого я надеялся достичь. Я закончил тем, что полностью опустился до nasm для этого эксперимента, но необходимость переписывать код с встроенных функций на простой asm было просто ненужным трением. Конечно, я хочу, чтобы оптимизатор обрабатывал такие вещи, как выбор инструкций или сворачивание констант при использовании некоторого высокоуровневого векторного API. Но когда я явно решил, какие инструкции использовать, я действительно не хочу, чтобы компилятор возился с этим. Единственная альтернатива - встроенный asm.

Поэтому, если API-интерфейсы поставщика реализованы в Rust, доступны на std::arch и могут быть объединены для решения проблемы, я согласен, что они лучше, чем встроенная сборка.

Это все, что я говорил сначала

выполнить 95-99% поставленных целей

и снова

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

Большинство людей ожидают, что [внутренние функции] достигнут 95-99% целей.

Это то же самое, что параллельно говорит на реалии текущей ситуации .

Я

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

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

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

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

cc @ rust-lang / lang

@eddyb _require_ - сильное слово, и я вынужден сказать, что нет, от нас строго не требуется использовать встроенный ассемблер. Как я упоминал ранее, мы _ можем_ определять процедуры на чистом ассемблере, собирать их отдельно и связывать их с нашими программами на Rust через FFI.

Однако, как я сказал ранее, я не знаю ни одного серьезного проекта уровня ОС, который бы это делал. Это будет означать множество шаблонов (читай: больше шансов на ошибку), более сложный процесс сборки (прямо сейчас нам повезло, что мы можем обойтись простым вызовом cargo и связанным и почти готовое к запуску ядро ​​выскакивает из другого конца; нам пришлось бы вызвать ассемблер и связать на отдельном шаге), и резкое снижение способности встраивать вещи и т. д .; почти наверняка будет удар по производительности.

Такие вещи, как встроенные функции компилятора, помогают во многих случаях, но для таких вещей, как набор инструкций надзора целевой ISA, особенно более эзотерических аппаратных функций (например, функций гипервизора и анклава), часто нет встроенных функций, и мы находимся в среда no_std. Каких внутренних компонентов часто бывает недостаточно; например, соглашение о вызове прерывания x86 кажется крутым, но не дает вам изменяемого доступа к регистрам общего назначения в кадре ловушки: предположим, я беру неопределенное исключение инструкции с намерением выполнить эмуляцию, и предположим, что эмулированная инструкция возвращает значение в% rax или что-то в этом роде; соглашение о вызовах не дает мне хорошего способа передать это обратно на сайт вызова, поэтому нам пришлось свернуть свое собственное. Это означало, что я написал свой собственный код обработки исключений на ассемблере.

Так что, честно говоря, нет, мы не _require_ встроенный ассемблер, но он достаточно полезен, поэтому было бы почти не для начала не иметь его.

@dancrossnyc Мне особенно интересно избегать отдельной сборки, то есть того, какая сборка вам вообще нужна в вашем проекте, независимо от того, как вы ее связываете.

В вашем случае это похоже на привилегированное подмножество ISA супервизора / гипервизора / анклава, это правильно?

часто нет внутренних

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

и мы находимся в среде no_std

Для записи, встроенные функции поставщика присутствуют как в std::arch и в core::arch (первое - это реэкспорт).

соглашение о вызове прерывания x86 кажется крутым, но не дает вам изменяемого доступа к регистрам общего назначения в кадре ловушки

cc @rkruppe Можно ли это реализовать в LLVM?

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

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

В какой-то степени оба верны, но в целом я бы сказал, что последнее здесь более уместно. Некоторые вещи специфичны для микроархитектуры и зависят от конкретной конфигурации пакета процессора. Было бы разумно, чтобы компилятор (например) раскрыл что-то как внутреннее, что является частью подмножества привилегированных инструкций _и_ обусловлено конкретной версией процессора? Честно говоря, не знаю.

Для записи, встроенные функции вендора находятся как в std :: arch, так и в core :: arch (первое - это реэкспорт).

Это действительно полезно знать. Спасибо!

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

Мы уже делаем. Например, инструкции xsave x86 реализованы и представлены в core::arch , доступны не на всех процессорах, и для большинства из них требуется привилегированный режим.

@gnzlbg xsave не имеет привилегий; вы имели ввиду xsaves ?

Я просмотрел https://rust-lang-nursery.github.io/stdsimd/x86_64/stdsimd/arch/x86_64/index.html и единственные привилегированные инструкции, которые я увидел в своем быстром обзоре (я не делал исчерпывающий поиск) были xsaves , xsaves64 , xrstors и xrstors64 . Я подозреваю, что это внутренние компоненты, потому что они относятся к общему семейству XSAVE* и не генерируют исключения в реальном режиме, а некоторые люди хотят использовать clang / llvm для компиляции кода реального режима.

@dancrossnyc да, некоторые из них я имел в виду (мы реализуем xsave , xsaves , xsaveopt , ... в модуле xsave : https: //github.com/rust-lang-nursery/stdsimd/blob/master/coresimd/x86/xsave.rs).

Они доступны в core , поэтому вы можете использовать их для написания ядра ОС для x86. В пользовательском пространстве они бесполезны, AFAICT (они всегда вызывают исключение), но у нас нет способа отличить это в core . Мы могли выставить их только в core но не в std , но поскольку они уже стабильны, этот корабль отплыл. Кто знает, может быть, какая-нибудь ОС когда-нибудь запустит все в кольце 0, и вы сможете использовать их там ...

@gnzlbg Я не знаю, почему xsaveopt или xsave вызывают исключение в пользовательском пространстве: xsaves - единственный из семейства, который определен для генерации исключения (#GP если CPL> 0), и то только в защищенном режиме (SDM vol.1 ch. 13; vol.2C ch. 5 XSAVES). xsave и xsaveopt полезны для реализации, например, упреждающих потоков пользовательского пространства, поэтому их присутствие в качестве встроенных функций действительно имеет смысл. Я подозреваю, что внутренняя функция для xsaves была либо потому, что кто-то просто добавил все из семейства xsave не осознавая проблемы с привилегиями (то есть, предполагая, что это было вызвано из пользовательского пространства), либо кто-то хотел назвать это из реального режима. Последнее может показаться надуманным, но я знаю, что люди, например, создают микропрограммы реального режима с помощью Clang и LLVM.

Не поймите меня неправильно; наличие встроенных функций LLVM в core - это здорово; Если мне никогда не придется писать эту глупую последовательность инструкций, чтобы снова получить результаты rdtscp в полезном формате, я буду счастлив. Но текущий набор встроенных функций не заменяет встроенный ассемблер, когда вы пишете ядро ​​или другие средства контроля.

@dancrossnyc, когда я упомянул xsave я имел в виду некоторые встроенные функции, которые доступны за битами CPUID XSAVE, XSAVEOPT, XSAVEC и т. д. Некоторые из этих встроенных функций требуют привилегированного режима.

Было бы разумно, чтобы компилятор (например) раскрыл что-то как внутреннее, что является частью подмножества привилегированных инструкций и зависит от конкретной версии процессора?

Мы уже делаем, и они доступны в стабильной версии Rust.

Я подозреваю, что внутренняя для xsaves была либо потому, что кто-то просто добавил все из семейства xsave, не осознавая проблемы с привилегиями

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

Но текущий набор встроенных функций не заменяет встроенный ассемблер, когда вы пишете ядро ​​или другие средства контроля.

Согласен, поэтому этот вопрос пока открыт;)

@gnzlbg, извините, я не хочу сорвать это из-за того, что займется xsave и др.

Однако, насколько я могу судить, единственные встроенные функции, требующие привилегированного выполнения, - это те, которые связаны с xsaves и даже тогда они не всегда являются привилегированными (опять же, для реального режима все равно). Замечательно, что они доступны в стабильном Rust (серьезно). Остальные могут быть полезны в пользовательском пространстве, и я думаю, что это здорово, что они там есть. Однако xsaves и xrstors - это очень, очень небольшая часть привилегированного набора инструкций, и добавление встроенных функций для двух инструкций качественно отличается от того, как это делается в целом, и я думаю, что остается вопрос: это уместно _в целом_. Рассмотрим, например, инструкцию VMWRITE из расширений VMX; Я предполагаю, что внутренняя функция будет делать что-то вроде инструкции «выполнить», а затем «вернуть» rflags . Это своего рода странно специализированная вещь, которую нужно иметь как внутреннюю.

Я думаю, что в противном случае мы здесь согласны.

FWIW согласно std::arch RFC в настоящее время мы можем добавлять только встроенные функции к std::arch которые поставщики предоставляют в своих API. В случае xsave Intel предоставляет их в своем API C , поэтому это нормально. Если вам нужны какие-либо встроенные функции поставщика, которые в настоящее время не представлены, откройте вопрос, требуется ли для этого привилегированный режим или нет, не имеет значения.

Если поставщик не предоставляет для него встроенную функцию, тогда std::arch может не подходить для этого, но есть много альтернатив этому (встроенная сборка, глобальный asm, вызов C, ...).

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

В любом случае, да, я думаю, что мы принципиально согласны: встроенные функции не подходят для всего, и поэтому мы хотели бы, чтобы asm! () Был переведен в стабильную версию. Я очень рад услышать, что в этой области наблюдается прогресс, как вы сказали вчера, и если мы сможем мягко подтолкнуть

Несколько дополнительных деталей и примеров использования для asm! :

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

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

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

+ 1e6

@eddyb

Хорошо, я попробую внутренний подход и посмотрю, чего он добьется. Вы, наверное, правы, и это лучший подход для моего случая. Спасибо!

@joshtriplett отлично справился ! Это точные варианты использования, которые я имел в виду.

loop {
   :thumbs_up:
}

Я бы добавил еще пару вариантов использования:

  • написание кода в странных архитектурных режимах, таких как вызовы BIOS / EFI и 16-битный реальный режим.
  • написание кода со странными / необычными режимами адресации (который часто возникает в 16-битном реальном режиме, загрузчиках и т. д.)

@ mark-im Совершенно верно! И обобщение вопроса, который имеет подварианты в обоих наших списках: перевод между соглашениями о вызовах.

Я закрываю # 53118 в пользу этого вопроса и копирую PR здесь для записи. Обратите внимание, что это с августа, но беглый взгляд, кажется, показывает, что ситуация не изменилась:


Раздел о поточной сборке требует капитального ремонта; в его нынешнем состоянии это означает, что поведение и синтаксис привязаны к rustc и языку rust в целом. Практически вся документация посвящена сборке x86 / x86_64 с набором инструментов llvm. Для ясности, я имею в виду не сам код сборки, который явно зависит от платформы, а скорее общую архитектуру и использование встроенной сборки в целом.

Я не нашел авторитетного источника поведения встроенной сборки, когда дело доходит до цели ARM, но, судя по моим экспериментам и ссылкам на документацию встроенной сборки ARM GCC , следующие моменты кажутся полностью отключенными:

  • Синтаксис ASM, поскольку ARM / MIPS (и большинство других CISC?) Использует синтаксис Intel-esque с регистром назначения первым. Я понял, что документация означает / подразумевает, что встроенный asm использует синтаксис & t, который был перенесен в фактический синтаксис, специфичный для платформы / компилятора, и что я должен просто заменить имена регистров x86 только регистрами ARM.
  • Соответственно, параметр intel недействителен, так как вызывает ошибки "неизвестной директивы" при компиляции .
  • Адаптация документации встроенной сборки ARM GCC (для сборки с thumbv7em-none-eabi с помощью инструментальной цепочки arm-none-eabi-* , похоже, что даже некоторые базовые предположения о формате встроенной сборки зависят от платформы. В частности, это Кажется, что для ARM выходной регистр (второй аргумент макроса) считается ссылкой на регистр, т.е. $0 относится к первому выходному регистру, а не к первому входному регистру, как в случае с инструкциями x86 llvm.
  • В то же время другие специфичные для компилятора особенности _не_ присутствуют; Я не могу использовать именованные ссылки на регистры, только индексы (например, asm("mov %[result],%[value],ror #1":[result] "=r" (y):[value] "r" (x)); недействителен).
  • (Даже для целей x86 / x86_64 использование $0 и $2 в примере встроенной сборки очень сбивает с толку, так как не объясняет, почему были выбраны эти числа.)

Думаю, больше всего меня поразило заключительное заявление:

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

Что, кажется, не всегда верно.

Я понял, что документация означает / подразумевает, что встроенный asm использует синтаксис & t, который был перенесен в фактический синтаксис, специфичный для платформы / компилятора, и что я должен просто заменить имена регистров x86 только регистрами ARM.

Понятие синтаксиса intel vs at & t существует только на x86 (хотя могут быть и другие случаи, о которых я не знаю). Он уникален тем, что это два разных языка, использующих один и тот же набор мнемоник для представления одного и того же набора двоичного кода. Экосистема GNU установила синтаксис at & t как доминирующий по умолчанию для мира x86, поэтому это то, что по умолчанию используется встроенным asm. Вы ошибаетесь в том, что это в значительной степени прямая привязка к встроенным выражениям ассемблера LLVM, которые, в свою очередь, в основном просто выгружают открытый текст (после обработки подстановок) в текстовую программу сборки. Ничто из этого не является уникальным (или даже актуальным) для сегодняшнего asm!() относительно него, поскольку оно полностью зависит от платформы и совершенно бессмысленно за пределами мира x86.

Соответственно, параметр intel недействителен, так как вызывает ошибки "неизвестной директивы" при компиляции.

Это прямое следствие "тупой" / простой вставки открытого текста, о которой я говорил выше. Как указано в сообщении об ошибке, директива .intel_syntax не поддерживается. Это старый и хорошо известный обходной путь для использования встроенного asm в стиле Intel с GCC (который испускает стиль att): нужно просто написать .intel_syntax в начале встроенного блока asm, а затем написать немного информации style asm и, наконец, завершиться с помощью .att_syntax чтобы вернуть ассемблер в режим att, чтобы он снова правильно обработал (следующий) код, сгенерированный компилятором. Это грязный взлом, и я помню, по крайней мере, у реализации LLVM в течение долгого времени были некоторые странные причуды, поэтому похоже, что вы видите эту ошибку, потому что она наконец была удалена. К сожалению, единственный правильный способ действий здесь - удалить опцию "intel" из rustc.

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

Ваше наблюдение совершенно верно, каждая платформа составляет как свой собственный двоичный формат, так и свой собственный язык ассемблера. Они полностью независимы и (по большей части) не обрабатываются компилятором - в этом весь смысл программирования на чистом ассемблере!

Я не могу использовать именованные ссылки на регистры, только индексы

К сожалению, существует довольно большое несоответствие между встроенной asm-реализацией LLVM, которую предоставляет rustc, и реализацией GCC (которая эмулирует clang). Без решения о том, как продвигаться вперед с asm!() нет особой мотивации улучшать это - кроме того, я уже давно обрисовал основные варианты, все они имеют явные недостатки. Поскольку это не кажется приоритетом, вы, вероятно, застрянете с сегодняшними asm!() по крайней мере на несколько лет. Есть достойные обходные пути:

  • полагаться на оптимизатор для создания оптимального кода (с небольшим подталкиванием вы обычно можете получить именно то, что хотите, даже не написав необработанную сборку самостоятельно)
  • используйте встроенные функции, еще одно довольно элегантное решение, которое лучше, чем встроенный asm почти во всех отношениях (если вам не нужен точный контроль над выбором и планированием инструкций)
  • вызвать ящик cc из build.rs чтобы связать объект C со встроенным asm

    • в основном просто вызывайте любой ассемблер, который вам нравится, из build.rs , использование компилятора C может показаться излишним, но избавляет вас от хлопот интеграции с системой build.rs

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

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

вызвать cc crate из build.rs, чтобы связать объект C со встроенным asm

Вы также можете вызвать ящик cc из build.rs для создания простых файлов сборки, что дает максимальный контроль. Я настоятельно рекомендую сделать именно это на случай, если два вышеперечисленных «обходных пути» не сработают для вашего варианта использования.

@ main-- написал:

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

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

(У меня есть еще один вариант использования: я хотел бы когда-нибудь научить системному программированию компьютерную архитектуру и прочее, используя Rust вместо C.Отсутствие встроенной сборки сделало бы это намного более неудобным.)

Я бы хотел, чтобы мы сделали встроенную сборку приоритетом в Rust и стабилизировали ее раньше, чем позже. Может быть, это должно быть целью Rust 2019. Меня устраивает любое из решений, которые вы перечисляете в своем приятном комментарии ранее : я могу жить с проблемами любого из них. Возможность встроенного ассемблерного кода для меня является предпосылкой для написания Rust вместо C везде: мне действительно нужно, чтобы он был стабильным.

Я бы хотел, чтобы мы сделали встроенную сборку приоритетом в Rust и стабилизировали ее раньше, чем позже. Может быть, это должно быть целью Rust 2019.

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

Чтобы прояснить мой комментарий выше - проблема в том, что в документации не объясняется, насколько «глубоко» содержимое макроса asm!(..) анализируется / взаимодействует с ним. Я знаком с x86 и сборкой MIPS / ARM, но предполагал, что llvm имеет собственный формат языка ассемблера. Раньше я использовал встроенную сборку для x86, но было неясно, до какой степени пошло ублюдение asm до C и ASM. Мое предположение (теперь признанное недействительным), основанное на формулировке в разделе встроенной сборки ржавчины, заключалось в том, что LLVM имеет собственный формат ASM, который был создан для имитации сборки x86 в режимах at & t или Intel и обязательно выглядел как показанные примеры x86.

(Мне помогло изучение расширенного вывода макроса, который прояснил, что происходит)

Я думаю, что на этой странице должно быть меньше абстракции. Сделайте понятнее, что анализирует LLVM, а что интерпретирует напрямую как ASM. Какие части являются специфичными для ржавчины, какие части специфичны для оборудования, на котором вы работаете, и какие части принадлежат клею, который их скрепляет.

вызвать ящик cc из build.rs чтобы связать объект C со встроенным asm

Недавний прогресс в кросс-языковом LTO заставляет меня задуматься, можно ли уменьшить некоторые из недостатков этого пути, эффективно встраивая этот «внешний блок сборки». ( вероятно, нет )

вызвать ящик cc из build.rs чтобы связать объект C со встроенным asm

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

Даже если это сработает, я не хочу писать свою встроенную сборку на C. Я хочу написать ее на Rust. :-)

Я не хочу писать свою встроенную сборку на C.

Вы можете скомпилировать и связать файлы .s и .S напрямую (см., Например, этот ящик ), которые в моей книге находятся достаточно далеко от C. :)

если некоторые из недостатков этого проспекта можно уменьшить

Я считаю, что в настоящее время это невозможно, поскольку межъязыковая LTO полагается на наличие LLVM IR, и сборка не сгенерирует это.

Я считаю, что в настоящее время это невозможно, поскольку межъязыковая LTO полагается на наличие LLVM IR, и сборка не сгенерирует это.

Вы можете вставить сборку в сборку на уровне модуля в модулях LLVM IR.

Кто-нибудь знает, какое последнее предложение / текущий статус? Поскольку тема года - «зрелость и завершение того, что мы начали», это кажется прекрасной возможностью, наконец, закончить asm .

Неясные планы относительно нового (подлежащего стабилизации) синтаксиса обсуждались в феврале прошлого года: https://paper.dropbox.com/doc/FFI-5NmXV30TGiSsr9dIxpqpq

Согласно этим заметкам @joshtriplett и @Amanieu подписались для написания RFC.

Каков статус нового синтаксиса?

Это должно быть RFC и реализовано каждую ночь.

ping @joshtriplett @Amanieu Дайте мне знать, могу ли я помочь сдвинуть дело с мертвой точки ! Я свяжусь с вами в ближайшее время.

@cramertj AFAICT любой может продвинуть это вперед, это разблокировано и ждет, когда кто-то

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


РЕДАКТИРОВАТЬ: чтобы было ясно, под убеждением я конкретно имею в виду такие части предварительного RFC, как этот:

дополнительно при необходимости добавляются сопоставления для классов регистров (см. llvm-constraint 6)

где есть десятки классов регистров, специфичных для архитектуры, в lang-ref. RFC не может просто отмахнуться от всего этого, и убедившись, что все они работают так, как должны, или имеют смысл, или достаточно "стабильны" в LLVM, чтобы их можно было раскрыть отсюда и т. Д., Будет полезно реализовать такую ​​реализацию. просто попробуйте это.

Поддерживается ли здесь встроенная сборка RISC-V с помощью #![feature(asm)] ?

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

Да, RISC-V поддерживается. Зависящие от архитектуры классы ограничений ввода / вывода / LLVM langref .

Однако есть предостережение - если вам нужно ограничить отдельные регистры в ограничениях ввода / вывода / затирания, вы должны использовать имена архитектурных регистров (x0-x31, f0-f31), а не имена ABI. В самом фрагменте сборки вы можете использовать любой вид имени регистра.

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

Я очень запутался:

  • Если вы пишете asm, не следует ли его переписывать (человеком с #[cfg(...)] ) для каждой архитектуры _и backend_, которую вы пытаетесь поддерживать?
  • Это означает, что вопрос о «синтаксисе» спорный ... просто используйте синтаксис для этой архитектуры и бэкэнд, которые использует компилятор.
  • Rust просто потребуются небезопасные функции std, чтобы иметь возможность помещать байты в правильные регистры и отправлять / извлекать в стек для любой архитектуры, для которой выполняется компиляция - опять же, это, возможно, придется переписать для каждой архитектуры и, возможно, даже для каждого бэкэнда.

Я понимаю, что обратная совместимость - это проблема, но с огромным количеством ошибок и тем фактом, что это никогда не было стабилизировано, возможно, было бы лучше просто передать это бэкэнду. Rust не должен заниматься исправлением ошибок синтаксиса LLVM, gcc или кого-либо еще. Rust занимается созданием машинного кода для архитектуры и компилятора, на которые он нацелен ... и asm уже фактически является этим кодом!

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

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

@ main-- сказал:

Обе оптимизации, безусловно, обеспечивают лучшую пропускную способность (особенно в условиях гиперпоточности), но не сокращение задержки, которого я надеялся достичь. Я закончил тем, что полностью опустился до nasm для этого эксперимента, но необходимость переписывать код с встроенных функций на простой asm было просто ненужным трением. Конечно, я хочу, чтобы оптимизатор обрабатывал такие вещи, как выбор инструкций или сворачивание констант при использовании некоторого высокоуровневого векторного API. Но когда я явно решил, какие инструкции использовать, я действительно не хочу, чтобы компилятор возился с этим. Единственная альтернатива - встроенный asm.

Возможно, вместо встроенного asm нам действительно нужны атрибуты функций для LLVM, которые сообщают оптимизатору: «оптимизируйте это для пропускной способности», «оптимизируйте это для задержки», «оптимизируйте это для двоичного размера». Я знаю, что это решение является апстримом, но оно не только решит вашу конкретную проблему автоматически (путем обеспечения более низкой задержки, но в противном случае изоморфной реализации алгоритма), но также позволит программистам на Rust иметь более точный контроль над характеристиками производительности. это имело значение.

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

@ mark-im, конечно, нет. Вот почему я цитирую буквально! 🙂

Я хотел сказать, что даже если вы можете решить «оптимизацию компилятора способом, противоположным тому, что мне нужно» (что в их случае является классическим: задержка или пропускная способность), используя встроенные функции asm, возможно (и определенно) этот вариант использования лучше обслуживается более детальным контролем оптимизаций :)

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

  • Проблема отслеживания встроенной сборки в стиле LLVM ( llvm_asm ) # 70173
  • Проблема отслеживания встроенной сборки ( asm! ) # 72016
Была ли эта страница полезной?
0 / 5 - 0 рейтинги