Можно ли создавать потоки без системных вызовов в сборке Linux x86 GAS? - PullRequest
33 голосов
/ 03 апреля 2009

При изучении «языка ассемблера» (в Linux на архитектуре x86, использующей GNU в качестве ассемблера), одним из важных моментов была возможность использования системных вызовов . Эти системные вызовы очень удобны и иногда даже необходимы, поскольку ваша программа работает в пользовательском пространстве .
Однако системные вызовы довольно дороги с точки зрения производительности, поскольку требуют прерывания (и, конечно, системного вызова), что означает, что необходимо переключить контекст с вашей текущей активной программы в пользовательском пространстве на систему, работающую в пространстве ядра.

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

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

поэтому мой вопрос имеет два аспекта (с дополнительным бонусным вопросом под ним):

  • Можно ли написать ассемблер код, который может запускать несколько потоков одновременно на нескольких ядрах в один раз без необходимости системы звонки?
  • Получу ли я прирост производительности, если у меня действительно крошечные потоки (крошечные, как в общем времени выполнения потока), потерю производительности или это вообще не стоит усилий?

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

Ответы [ 7 ]

23 голосов
/ 15 июня 2009

Короткий ответ: ты не можешь. Когда вы пишете ассемблерный код, он запускается последовательно (или с ветвями) в одном и только одном логическом (то есть аппаратном) потоке. Если вы хотите, чтобы часть кода выполнялась в другом логическом потоке (будь то на том же ядре, на другом ядре на том же процессоре или даже на другом процессоре), вам нужно, чтобы ОС установила указатель инструкций другого потока ( CS:EIP) чтобы указать код, который вы хотите запустить. Это подразумевает использование системных вызовов, чтобы заставить ОС делать то, что вы хотите.

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

Редактировать: Включение ответа Ира Бакстера с Parlanse . Если вы убедитесь, что в вашей программе есть поток, работающий в каждом логическом потоке, то вы можете создать свой собственный планировщик, не полагаясь на ОС. В любом случае, вам нужен планировщик для обработки перехода от одного потока к другому. Между вызовами планировщика нет специальных инструкций по сборке для обработки многопоточности. Сам планировщик не может полагаться на какую-либо специальную сборку, а скорее на соглашения между частями планировщика в каждом потоке.

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

13 голосов
/ 16 июня 2009

«Доктор, доктор, мне больно, когда я так поступаю». Доктор: «Не делай этого».

Краткий ответ: многопоточное программирование можно выполнять без вызов дорогих примитивов управления задачами ОС. Просто игнорируйте ОС для потока планирование операций. Это означает, что вы должны написать свой собственный поток планировщик и просто никогда не передаст управление обратно ОС. (И вы должны быть как-то умнее насчет ваших потоков чем довольно умные парни ОС). Мы выбрали этот подход именно потому, что Windows обрабатывает / поток / волоконно-оптические вызовы были слишком дороги, чтобы поддерживать вычисления зерна нескольких сотен инструкций.

Наш язык программирования PARLANSE - это язык параллельного программирования: Смотри http://www.semdesigns.com/Products/Parlanse/index.html

PARLANSE работает под Windows, предлагает параллельные «зерна» как абстрактный параллелизм построить и планирует такие зерна сочетанием настроенный рукописный планировщик и код планирования, сгенерированный PARLANSE компилятор, который учитывает контекст зерна минимизировать планирование накладных расходов. Например, компилятор гарантирует, что регистры зерна не содержат информации в точке где может потребоваться планирование (например, «ждать»), и, таким образом, код планировщика должен только сохранить ПК и SP. По факту, довольно часто код планировщика вообще не контролируется; раздвоенное зерно просто хранит разветвление ПК и ИП, переключается на выделенный компилятором стек и переходит к зерну код. Завершение зерна перезапустит вилку.

Обычно есть блокировка для синхронизации зерна, реализовано компилятором, используя собственные инструкции LOCK DEC, которые реализуют что составляет подсчет семафоров. Приложения может логически раскошелиться на миллионы зерен; ограничения планировщика родительские зерна от создания большего количества работы, если рабочие очереди достаточно долго, поэтому больше работы не будет полезным. Планировщик реализует похищение работы, чтобы позволить неработающим процессорам захватывать готовые зерна образуют соседние рабочие очереди процессора. Это имеет был реализован для обработки до 32 процессоров; но мы немного обеспокоены что производители x86 могут на самом деле использовать более что в ближайшие несколько лет!

PARLANSE - зрелый язык; мы используем его с 1997 года, и внедрили в него приложение с несколькими миллионами параллельных линий.

7 голосов
/ 03 апреля 2009

Реализация потоков в пользовательском режиме.

Исторически, модели потоков обобщаются как N: M, то есть N потоков пользовательского режима, работающих на M потоков моделей ядра. Современное использование - 1: 1, но так было не всегда, и не обязательно.

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

5 голосов
/ 10 апреля 2009

Если вы хотите повысить производительность, вам придется использовать потоки ядра. Только ядро ​​может помочь вам запустить код одновременно на нескольких ядрах процессора. Если ваша программа не связана с вводом / выводом (или выполняет другие операции блокировки), выполнение совместной многопоточности в пользовательском режиме (также известной как fiber ) не принесет вам никакой производительности. Вы просто будете выполнять дополнительные переключения контекста, но один процессор, на котором работает ваш реальный поток, все равно будет работать на 100% в любом случае.

Системные вызовы стали быстрее. Современные процессоры поддерживают инструкцию sysenter, которая значительно быстрее, чем старая инструкция int. См. Также эту статью о том, как Linux выполняет системные вызовы максимально быстрым способом.

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

3 голосов
/ 14 октября 2016

Уже немного поздно, но я сам интересовался этой темой. На самом деле, нет ничего особенного в потоках, которые требуют, чтобы ядро ​​вмешивалось, КРОМЕ для распараллеливания / производительности.

Обязательный BLUF :

Q1: Нет. Для создания нескольких потоков ядра в разных ядрах / гиперпотоках ЦП необходимы как минимум начальные системные вызовы.

Q2: Это зависит. Если вы создаете / уничтожаете потоки, которые выполняют крошечные операции, то вы тратите впустую ресурсы (процесс создания потока будет значительно превышать время, используемое протектором до его выхода). Если вы создаете N потоков (где N - это ~ число ядер / гиперпотоков в системе) и повторно назначаете их, тогда ответ МОЖЕТ быть положительным в зависимости от вашей реализации.

Q3: Вы МОЖЕТЕ оптимизировать работу, если заранее ЗНАЕТЕ точный метод заказа операций. В частности, вы можете создать то, что составляет ROP-цепочку (или цепочку прямых вызовов, но на самом деле это может оказаться более сложным для реализации). Эта ROP-цепочка (как выполняемая потоком) будет непрерывно выполнять команды 'ret' (для своего собственного стека), где этот стек непрерывно добавляется (или добавляется в случае, когда он переносится в начало). В такой (странной!) Модели планировщик хранит указатель на «конец ROP-цепочки» каждого потока и записывает в него новые значения, в результате чего код обтекает память, выполняя код функции, что в конечном итоге приводит к команде ret. Опять же, это странная модель, но, тем не менее, интригует.

На мой контент на 2 цента.

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

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

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

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

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

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

Таким образом, действительно возможно (полностью в сборке и без системных вызовов, кроме исходных mmap и mprotect) создавать потоковые конструкции пользовательского режима в некорневом процессе.

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

3 голосов
/ 10 апреля 2009

Системные вызовы теперь не такие медленные, с syscall или sysenter вместо int. Тем не менее, при создании или уничтожении потоков будут накладные расходы. После запуска системные вызовы отсутствуют. Потоки пользовательского режима на самом деле вам не помогут, поскольку они работают только на одном ядре.

3 голосов
/ 10 апреля 2009

Сначала вы должны научиться использовать потоки в C (pthreads, POSIX theads). В GNU / Linux вы, вероятно, захотите использовать потоки POSIX или потоки GLib. Тогда вы можете просто вызвать C из кода сборки.

Вот несколько указателей:

...