Pull to refresh

Comments 57

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

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

Как Вы думаете, товарищи, какого синтаксического сахара, можно было бы добавить в данный язык, так, чтобы это не повлияло на производительность?
Макросы — великая вещь. Можно хоть в бейсик превратить. Какой еще сахар может быть в машинных инструкциях?
возможно тогда стоит FORTH исспользовать? Макроассемблеры не так гибки.
FORTH! Как много в этом звуке для сердца русского слилось! (с)
Для более гибкой гибкости есть C, для системного программирования большего и не нужно.
CleverMouse, похоже, тот ещё красноглазик (в самом положительном смысле). Чувствуется стремление к логичности и рациональности — в скурпулёзно откомментированном и отформатированном коде, в практически безошибочно написанной (и отформатированной) статье, в уважении к букве ё. У меня прям прокрастинация прошла — лечебная статья, хоть я особо и не силён в низкоуровневом программировании.
Это, скорее всего, свойственная для девушек аккуратность (:
Ну, если понимать аккуратность как стремление к упорядоченности, то она свойственна большинству тех людей, которым приходится работать с большими объёмами информации. Аккуратность, упорядоченность, архитектурность в подходах — без этого никуда, если хочешь сделать что-то действительно крупное и стоящее, например, операционную систему.
> в скурпулёзно откомментированном и отформатированном коде

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

но все-таки глядя на асм-код хочется взглянуть автору в глаза и спросить: «автор, ну зачем все это? ведь твой код на асм-x86 становится плохо переносимым, даже переход на 64-битную архитектуру вызовет переписку практически всего. не лучше ли писать на каком-нибудь высокоуровневом языке и обрщаться к асму лишь при острой необходимости?»
имхо, на асме иначе не выйдет писать.
если код не «радует глаз» — то глаза от всех этих cli; jmp $ в отместку сбегаются в кучку и желание работать пропадает напрочь.
Асмовский код код, который радует глаз — как правило совсем не оптимальный (с точки зрения производительности) код. Для того, чтобы планировщик процессора чувствовал себя хорошо и чтобы задержки (stalls) были минимальными, зависящие друг от друга инструкции нужно разносить, получается некая гребёнка из перемешанных инструкций, относящихся к разным задачам. Параллелизм на уровне планировщика. Посмотрите на код, который генерят современные оптимизирующие компиляторы — да там же месиво, местами очень трудно понимаемое. Вы скажите «а как же out-of-order execution, register renaming, etc. ?» — да, влияние на очень умные процессоры будет на таким значительным, но есть же ещё и Atom-ы. Написать маленькую функцию на асме, которая бы соперничала по скорости с компилятором можно, а большой проект — ИМХО, никак.
да, вы абсолютно правы.
но речь у нас шла не о реализации алгоритма USB, а о офоррмлении текста программы.

Оформление красивое. Для полного счастья было бы неплохо указывать размер непосредственных операндов в инструкциях push imm (например ehci.inc:333) push 32

И ещё интересно зачем (там же) используются inc eax; inc eax; и push 32; pop ecx;?
Вместо add eax,2 и mov ecx,32 соответственно? Так короче. inc eax однобайтовая, add eax,2 занимает 3 байта. mov с непосредственным операндом — одна из немногих команд, не имеющих опкода, где непосредственное значение хранится как байт и расширяется до требуемого размера в ходе выполнения, поэтому mov ecx,32 занимает 5 байт — один на опкод, 4 на значение 32 — в отличие от push 32 с двумя байтами — один на опкод, 1 на значение 32.
Это также медленнее на один-два такта, но конкретно этот участок выполняется один раз в жизни контроллера при инициализации, и несколько тактов роли не играют, а вот несколько байт заметны везде.
Ну сколько вы выиграете на всём ядре, килобайт? может быть два? А производительность приносите в жертву. Этот кусочек не единственный, вот в планировщике, подряд
        push    5
        pop     ecx
        push    1
        pop     ebx
        push    sizeof.ehci_static_ep
        pop     edx
а конструкции вида
        push imm
        pop reg32
        call proc
вообще сериализация. Ну и не понятно тогда, почему же, если вы оптимизируете под размер, то всё равно много где используется нормальная загрузка значения, как
        mov     ecx, 4
Где же однообразие кода и подхода?

Да вы и сами, наверняка, знаете, что не всё с кодом оптимально, но из-за ассемблера, изменения становятся вся более трудоёмкими, поэтому начинает преобладать принцип: «работает? — не лезь!»
А Си-шый код так легко перекомпилировать, и новым компилятором, и под новый процессор…
Сейчас использую ассемблер только для PIC-микроконтроллеров — вот там он востребован, так как счёт действительно идёт на байты.
Вы молодцы, ваш проект — замечательный пример программирования на ассемблере, школьникам и студентам для обучения вообще бомба. Но для реальных задач, ИМХО, Сизифов труд.
А производительность приносите в жертву.

Это не так. Производительность не меняется. Даже если вы можете считать отдельные такты, вы собьётесь со счёта в момент сброса контроллера — он занимает больше, чем вся остальная инициализация, вместе взятая, — а 100-миллисекундный интервал, требуемый перед началом какой бы то ни было работы с USB-устройством, покажется вам вечностью.
Ну и не понятно тогда, почему же, если вы оптимизируете под размер, то всё равно много где используется нормальная загрузка значения, как

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

… и какая разница, что размер результата будет в разы больше…
Да вы и сами, наверняка, знаете, что не всё с кодом оптимально, но из-за ассемблера, изменения становятся вся более трудоёмкими, поэтому начинает преобладать принцип: «работает? — не лезь!»

Вы серьёзно хотите продемонстрировать тезис о трудоёмкости изменений задачей, решаемой заменой по регэкспу? Специально для вас в свежей ревизии ядра я заменила пары push / pop на специальный макрос, который можно определить как простой mov, а можно определить так, как он сейчас определён — парой push / pop. Для критичных ко скорости участков по-прежнему есть mov.
По производительности — второй пример был из планировщика ehci_select_hs_interrupt_list — там же не вызывается сброс контроллера каждый раз?

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

И насчёт «размер результата будет в разы больше» это вы погорячились. Я тоже так думал, ровно до тех пор, пока компилятор не стал генерить сравнимый (а зачастую и более короткий и быстрый код). Учитывая разницу во времени на получение этого результата, я крепко призадумался. Потом переписывал только маленькие критические кусочки, потом интринсики.
По производительности — второй пример был из планировщика ehci_select_hs_interrupt_list — там же не вызывается сброс контроллера каждый раз?

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

Трудоёмкость — да, очень хочу убедить

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

так как шишек много. Опять таки, это был первый попавшийся на глаза пример.

Это неубедительное теоретизирование. Убедительной демонстрацией было бы «вот, смотрите, я заменил movi на mov и теперь <что-нибудь> занимает не две секунды, а одну».

Если очень интересно — можно ещё найти места, где используются лишние копирования (потому что
человеку не под силу держать в голове текущий контекст программы)

Вот это интересно, подобное ещё и размер раздувает. Найдите — я исправлю и скажу «спасибо».

где инструкции идут не в самом благоприятном порядке — это всё макросами не исправишь

Инструкции можно переставить. Но здесь уже надо доказывать, что перестановка инструкций что-то даст.

(movi — оперативненько!)

Спасибо. Я стараюсь.

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

Распространённое заблуждение. Я приведу два примера.

Пример 1. У нас есть драйверы для видеокарт Intel и ATI, портированные с Linux, — естественно, на Си. Их нет в дистрибутиве: в образ они не влезают, а методы, позволяющие обратиться к дополнительным источникам данных, где бы они ни были, пока в разработке — но их без проблем можно найти на форуме. Так вот, дословный кусок кода из одной версии одного из драйверов:
.text:000033EA                 movzx   edx, byte ptr [eax+ebx+3]
.text:000033EF                 shl     edx, 8
.text:000033F2                 movzx   esi, byte ptr [eax+ebx+2]
.text:000033F7                 or      esi, edx
.text:000033F9                 shl     esi, 10h
.text:000033FC                 movzx   edx, byte ptr [eax+ebx+1]
.text:00003401                 shl     edx, 8
.text:00003404                 movzx   eax, byte ptr [eax+ebx]
.text:00003408                 or      eax, edx
.text:0000340A                 movzx   eax, ax
.text:0000340D                 or      esi, eax

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

Пример 2. Типичный фрагмент программы на Си:
extern void f(void* something, int x, int y, int z);
...
void* p;
...
f(p, 1, 2, 3);

Вменяемый ассемблерщик в зависимости от желания/наличия макросов может написать либо
ccall f,[p],1,2,3

либо, что то же самое,
push 3
push 2
push 1
push [p]
call f
add esp,10h

Если p — локальная переменная и создан кадр стека, то [p] — что-то типа [ebp-4] и конструкция занимает 2*3+3+5+4 = 18 байт.
Барабанная дробь, gcc с ключом -Os, который якобы означает «оптимизировать по размеру»:
mov eax,[p]
mov dword[esp+12],3
mov dword[esp+8],2
mov dword[esp+4],1
mov dword[esp],eax
call f

Здесь кадра стека уже не будет, и [p] — что-то типа [esp+20]. Теперь конструкция занимает 4+8*3+3+5 = 36 байт. Разница, как видно, в два раза. Сравнимый код, говорите?
Пример 2. Типичный фрагмент программы на Си:...gcc с ключом -Os, который якобы означает «оптимизировать по размеру»

… у меня генерирует следующее:
$ cat | gcc -Os -xc - -S -m32 -g0 -o -
extern void f(void* something, int x, int y, int z);
void* p;
void g(void)
{
        f(p, 1, 2, 3);
}
        .file   ""
        .text
        .globl  g
        .type   g, @function
g:
.LFB0:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        subl    $8, %esp
        pushl   $3
        .cfi_escape 0x2e,0x4
        pushl   $2
        .cfi_escape 0x2e,0x8
        pushl   $1
        .cfi_escape 0x2e,0xc
        pushl   p
        .cfi_escape 0x2e,0x10
        call    f
        addl    $16, %esp
        .cfi_escape 0x2e,0
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
.LFE0:
        .size   g, .-g
        .comm   p,4,4
        .ident  "GCC: (GNU) 4.6.3 20120306 (Red Hat 4.6.3-2)"
        .section        .note.GNU-stack,"",@progbits

что в точности соответствует… Может вы его готовить не умеете?
$ cat | gcc -Os -xc - -S -m32 -g0 -o -
extern void f(void* something, int x, int y, int z);
void* p;
void g(void)
{
        f(p, 1, 2, 3);
}
        .file   ""
        .text
.globl _g
        .def    _g;     .scl    2;      .type   32;     .endef
_g:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    _p, %eax
        movl    $3, 12(%esp)
        movl    $2, 8(%esp)
        movl    $1, 4(%esp)
        movl    %eax, (%esp)
        call    _f
        leave
        ret
        .comm   _p, 4, 2
        .def    _f;     .scl    2;      .type   32;     .endef
$ gcc --version
gcc (GCC) 4.5.3
Copyright (C) 2010 Free Software Foundation, Inc.
Это свободно распространяемое программное обеспечение. Условия копирования
приведены в исходных текстах. Без гарантии каких-либо качеств, включая
коммерческую ценность и применимость для каких-либо целей.
Что ж, по крайней мере сгенерированный ассемблерный код улучшается со временем без нашего вмешательства, в отличие от однажды написанного ассемблерного кода.
Увы, нет. Я сравнивала при настройке автосборки актуальные на тот момент версии веток gcc3 и gcc4, и gcc4 сливал с треском, передача параметров mov'ом при -Os — лишь наиболее наглядный пример. Я рада, что в gcc 4.6 конкретно эту деталь наконец-то починили, но это отнюдь не единственная деталь.

Кроме того, почитайте обсуждение — все фокусируются на быстродействии, а не на размере. В реальных проектах компиляция идёт не с -Os, а с -O2 — ведь большой размер — это даже престижно.
Кроме того, почитайте обсуждение — все фокусируются на быстродействии, а не на размере. В реальных проектах компиляция идёт не с -Os, а с -O2 — ведь большой размер — это даже престижно.
А у вас размер — это самоцель какая-то? На PC расширить память — не проблема, увеличить производительность — да. Для маленьких/встраиваемых устройств, там где размер имеет значение, компилируется с -Os (например в XCode под iOS Release).

По производительности — скажите, где у вас профайлер показывает затыки, и я попробую пооптимизировать. Но опять таки, под какой процессор, какую память?
У нас нет ни профайлера, ни затыков :-) Мы даже никого не заставляем использовать KolibriOS. Просто рассказываем о себе, чтобы те, кому мы понравились или нужны, узнал о том, что мы есть, и как нас найти.
Увы, нет.

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

все фокусируются на быстродействии, а не на размере

Мне показалось, что обсуждение фокусируется на гибкости: пропоненты компиляции говорят, что при разработке на С/… у вас есть выбор: чем компилировать, как компилировать, подо что компилировать.
Нет, сишный код раздувается просто от того, что его перекомпилировали gcc4 вместо gcc3.
Мне показалось, что обсуждение фокусируется на гибкости

Я отвечу не своей цитатой выше по ветке:
Ну сколько вы выиграете на всём ядре, килобайт? может быть два? А производительность приносите в жертву


у вас есть выбор: чем компилировать, как компилировать, подо что компилировать.

Если результат при любом из выборов получается хуже, чем при написании на ассемблере, то факт наличия выбора особого значения не имеет.
Кстати, вы не думали о том, чтобы работать над улучшением кода генерируемого gcc, вместо работы над конкретным ассемблерным кодом? На мой взгляд, это по всем параметрам более благородная задача.
Из-за сложности модификации кодогегерации в gcc появился llvm и clang именно туда большинство усилий оптимизаторов устремлено. Код gcc очень сложно расширяемый и с каждым годом теряет мейнтенеров.
Ок, «gcc» в моём комментарии можно заменить на «ваш любимый/ваш целевой компилятор С».
А Вы не думали о том, чтобы делать лучше свою операционную систему, вместо того, чтобы хаять нашу? Глядишь, наши разработчики так впечатлятся, что сами перейдут к Вам.
вместо того, чтобы хаять нашу

Интересный поворот. Не имел намерений. Даже странно, какая моя фраза вас так задела.
По части фантома — я позанимался тем, что мне было в нём интересно. Пока продолжать большого смысла нет.
С другой моей операционной системой всё в порядке, скоро будет SMP, можно переходить (:
Ну, хотя бы совет, данный CleverMouse, «работать над улучшением кода генерируемого gcc, вместо работы над конкретным ассемблерным кодом». Мы же Вам не говорим, чем Вам заниматься, и не считаем Ваши занятия недостаточно благородными. За пример с GCC 4.6.3 Вам спасибо, кстати.
Ну вообще-то это был вопрос, и мне действительно интересно, что об этом думает CleverMouse.
Я в некоторый момент думала над улучшением одного момента в кодогенерации gcc. Потом я заглянула в исходники, закрыла их и бросила все мысли в этом направлении.
Упражнение на понимание:
Как я писал выше, в асме очень важен контекст. В данном случае у меня есть предположение, что где-то выше, эти данные были записаны как байты, а значит читать их двойным словом будет медленно. Не нравятся кэшам такие «оптимизации».

С компилятором GCC: (Gentoo 4.7.3 p1.0, pie-0.5.5) 4.7.3" код такой же, что и у jcmvbkbc.

И ещё интересная инфа, родной маковский nasm (NASM version 0.98.40 (Apple Computer, Inc. build 11) compiled on Feb 6 2013) кодирует push imm8 (без префикса размера) в 5 байт, также как если указать DWORD, а вот с BYTE — в два байта.
Если всё же нужно прочитать двойное слово и аппаратура умеет это делать, то программная эмуляция действий аппаратуры будет заведомо медленнее самой аппаратуры. Но я привела этот код в качестве примера к тезису «код раздувается в разы».

Ядро KolibriOS написано на fasm, а не на nasm, и с точки зрения fasm инструкция «push byte 1» недопустима, потому что не существует опкода, кладущего в стек именно байт.
Если всё же нужно прочитать двойное слово и аппаратура умеет это делать, то программная эмуляция действий аппаратуры будет заведомо медленнее самой аппаратуры.
В таком случае не было многотомных Optimization Guide-ов, лекций и семинаров посвящённых данному вопросу, так как следую вашей логике — раз аппаратура умеет сделать что-то одной инструкцией, то нечего пытаться заменить это несколькими?
Ну что же, контекст: это разбор таблиц AtomBIOS, в которые ни ядро, ни драйвер не пишут ни байтами, ни как бы то ни было ещё. Теперь сможете привести конкретную цитату из «многотомных Optimization Guide-ов», которая бы оправдывала такой код?
BIOS на видеокартах — это обычно Serial EEPROM, который отображается в адресное пространство процессора страницами со специальными атрибутами. Это при условии, что пользователь не установил в своих настройках BIOS-а материнской платы галочку "Video ROM BIOS Shadow", тогда всё шоколадно.
Доступ к EEPROM памяти, находящейся на другой плате, через несколько последовательных шин — довольно небыстрая процедура, ширина — 8 бит (да, да — те самые байты), читать надо подряд, иначе процессор обидится, и придётся чуток подождать.

Таким образом, вы должны гордится автором того кусочка, так как он сделал минимально возможный размер кода, исключив функцию определения типа страницы памяти и не дублируя приведённый кусочек кода (он же там больше) под разные сценарии.
Я не совсем понял объяснение — «mov ecx,32» и байт больше занимает, и выполняется медленнее на 1-2 такта, чем «push 32; pop ecx;», или байт занимает больше, но выполняется быстрее?
Оно длиннее, но быстрее. Альтернативный вариант короче, но медленнее.
Если добавить к ассемблеру достаточное для более-менее комфортного использования количество сахара, получится ANSI C 89 :)
При правильном применении производительность почти та же.
Ну, или урезаные версии, вроде C--
Хм… а как вы думаете — ребята, которые Колибри пишут, просто не в курсе, что «производительность почти та же», или у них другое мнение?

Может, им стоило бы как раз на Анси-С или С-- все писать?.. И гибко, и проще, вроде бы…
просто не в курсе, что «производительность почти та же», или у них другое мнение

Я думаю, среди них есть разные люди. Для кого-то это религия, для кого-то круто писать ОС на асме, кто-то хочет чтобы в проект не лезли не осилившие.
С точки зрения производительности 90% кода вообще всё равно на чём писать, если дизайн правильный.
Ну, я не в курсе насчёт именно Колибри, но писал под MenuetOS и там — да, все в курсе, но идея именно писать на ассемблере.
Тотальный контроль аппаратной части :)
Мне всегда было интересно, а нельзя ли, сохранив всю мощь макроассемблера как низкоуровневого языка, доработать его так, чтобы он еще больше походил на высокоуровневые языки, чтобы снизить порог вхождения при его изучении, и одновременно облегчить поддержку кода?

Вы сейчас говорите про С--
Периодически всплывает, иногда на нём даже что-то пишут.
Но win-а не получается. Люди обычно в обе стороны от него уходят. Кто-то в сторону чистого С (и выше), кто-то в сторону чистого ассемблера.
Почитал про него. Пожалуй, это примерно то, что я имел в виду. Правда, пишут, что он больше для промежуточной компиляции, нежели для непосредственного программирования…
Здорово. Значит, ход мысли у нас совпадает :D
UFO just landed and posted this here
Вопрос к CleverMouse
1) по какой причине стандартные драйвера для HID были включены в состав драйвера?
2) не проще ли сделать внешний интерфейс для драйвера, а драйвера реализовать как службы?
3) возможно было бы проще реализовать HID драйвера на С-- в качестве примера разделения логики контроллера USB от драйверов профилей?
1) Выделение части, работающей с HID, в отдельный драйвер имело бы смысл, если было бы несколько различных источников данных HID. Пока источник один и других не просматривается, это лишь ненужное усложнение.
2) Этот вопрос я не совсем поняла. Если «службой» вы называете отдельное usermode-приложение — фактически тот же драйвер, но в usermode — то писать сам драйвер проще бы не было — действия ровно те же самые. Возможно, было бы проще его отлаживать, несомненно, были бы куда легче последствия от того, что что-нибудь пошло не так. Ядру было бы, наоборот, сложнее. Я думаю над чем-нибудь типа libusb, но это явно неприоритетная вещь. Если вы имеете в виду что-то другое — уточните.
3) Ещё проще было бы ничего не делать. Естественно, это не позволило бы достичь цели. Если бы я ставила цель продемонстрировать какой-нибудь пример — скорее всего, я бы использовала Си как язык, понятный достаточно многим программистам и не слишком мешающий низкоуровневым вещам. Если бы я ставила цель реализовать пример какого бы то ни было USB-драйвера — я рассмотрела бы также вариант с той же libusb и, например, python, это бы ещё расширило круг читателей. Но моя цель — написать операционную систему возможно меньшего размера в той степени, пока это не мешает производительности, и C--, C, C++ не годятся для этого совсем никак.
1) Вопрос тогда закрыт
2) Вы все верно поняли — примерно таким же образом реализованы функции в QNX
3) Поскольку ответ на 1 вопрос закрыт — то 3 тоже не имеет смысла, раз разделения не требуется.
А у меня тоже вопрос к разработчикам: на сколько, по вашим личным ощущениям, более трудоёмко писать на асме, чем на том же c/c--?
Зависит и от разработчика, и от задачи.
В основном (на работе) я пишу на C и python, в Колибри преимущественно занимаюсь кодом тоже на Си. Когда я разрабатывал драйверы для принтера, я сначала написал прототип на python+libusb, а затем реализовал примерно то же на ассемблере (при помощи опытных людей, в частности, CleverMouse) — и был приятно удивлен тем, что задача, в общем-то, оказалась решена чуть ли не проще, чем на python.
Clever Mouse? Гаечка, так ты всё таки существуешь? Потому что написать реализацию USB на Ассемблере, по-моему, способна только одна умная мышь
Sign up to leave a comment.