Обзор и сравнительное тестирование ПЭВМ «Эльбрус 401‑PC». Часть третья — средства разработки

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

Вид системного блока Эльбрус 401-PC спереди и сбокуПример программы на машинном языке E2K

Напоминаем структуру статьи:

  1. обзор аппаратного обеспечения:
    • процесс приобретения;
    • аппаратное обеспечение;
  2. обзор программного обеспечения:
    • запуск операционной системы;
    • штатное программное обеспечение;
  3. обзор средств разработки:
  4. сравнительное тестирование производительности:
    • описание соперничающих компьютеров;
    • результаты бенчмарков;
    • подведение итогов.


Приятного чтения!
Сформулировать суть архитектуры E2K в одной фразе можно так: 64-битные регистры, явный параллелизм исполнения инструкций и строго контролируемый доступ к памяти.

Например, процессоры архитектуры x86 или SPARC, способные выполнять более одной инструкции за такт (суперскалярные), а иногда ещё и вне очерёдности, имеют неявный параллелизм: процессор прямо в реальном времени анализирует зависимости между инструкциями на небольшом участке кода и,  если считает возможным, нагружает те или иные исполнительные устройства одновременно, — иногда чересчур оптимистично (спекулятивно, с отбрасыванием результата или откатом транзакции в случае неудачного предсказания), иногда, наоборот, чересчур пессимистично (предполагая зависимости между значениями регистров или частей регистров, которых на самом деле нет с точки зрения исполняемой программы). При явном параллелизме тот же анализ проходит на этапе компиляции, и все машинные инструкции, определённые для параллельного исполнения, записываются в одно широкое командное слово (very large instruction word, VLIW), — причём у «Эльбруса» длина этого «слова» не фиксирована и может составлять от 1 до 8 двойных слов (в данном контексте, одинарное слово имеет разрядность 32 бита).

Бесспорно, компилятор располагает гораздо большими возможностями в плане объёма охватываемого кода, затрачиваемого времени и памяти, а при написании машинного кода вручную программист может осуществить ещё более интеллектуальную оптимизацию. Но это в теории, а на практике вы вряд ли будете использовать ассемблер, и потому всё упирается в то, насколько хорош оптимизирующий компилятор, а написать такой — задача не из простых, мягко говоря. К тому же, если при неявном параллелизме «медленные» инструкции могут продолжать работу, не блокируя поступление следующих инструкций на другие исполнительные устройства, то при явном параллелизме вся широкая команда будет ожидать завершения целиком. Наконец, оптимизирующий компилятор мало поможет при интерпретации динамических языков.

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

VLIW: прорыв или тупик?

Бытует мнение, что концепция VLIW плохо подходит для процессоров общего назначения: мол, выпускались же когда‑то Transmeta Crusoe — не «выстрелили». Автору странно слышать такие утверждения, так как десять лет назад он проводил тестирование ноутбука на базе Efficeon (это следующее поколение той же линейки) и нашёл его весьма многообещающим. Если не знать, что под капотом выполняется трансляция x86-кода в родные команды, догадаться об этом было невозможно. Да, тягаться с Pentium M он не мог, но производительность на уровне Pentium 4 показывал, причём потребление энергии было куда скромнее. И уж точно он был на голову выше VIA C3, который вполне себе x86.


Не меньший интерес из‑за своей экзотичности вызывает технология защищённого исполнения программ на языках C/C++, где использование указателей предоставляет обширный круг возможностей выстрелить себе в ногу. Концепция контекстной защиты, совместно реализуемая компилятором на этапе сборки и процессором на этапе выполнения, а также операционной системой в части управления памятью, не позволит нарушить область видимости переменных, — будь то обращение к закрытой переменной класса, частным данным другого модуля, локальным переменным вызывающей функции. Любые манипуляции с изменением уровня доступа допустимы только в сторону уменьшения прав. Блокируется сохранение ссылок на короткоживущие объекты в долгоживущих структурах. Предотвращаются также попытки использования зависших ссылок: если объект, на который некогда была получена ссылка, уже был удалён, то даже расположение другого, нового объекта по тому же адресу не будет считаться оправданием для доступа к его содержимому. Пресекаются поползновения использовать данные в качестве кода и передачи управления невесть куда.

Действительно, как только мы переходим от высокоуровневых идиом к низкоуровневым указателям, все эти области видимости оказываются не более чем синтаксической солью. Некоторые (простейшие) случаи ошибочного использования указателей иногда могут помочь отловить статические анализаторы исходного кода. Но когда программа уже оттранслирована в машинные инструкции x86 или SPARC, то ничто не помешает ей прочитать или записать значение не из той ячейки памяти или не того размера, что приведёт к краху совсем в другом месте, — и вот ты сидишь, смотришь на запорченный стек и понятия не имеешь, откуда начинать отладку, ведь на другой машине тот же код отрабатывает удачно. А переполнение стека и возникающие в следствие этого уязвимости — просто бич популярных платформ. Отрадно, что наши разработчики системно подходят к решению этих проблем, а не ограничиваются расстановкой всё новых и новых костылей, эффект от которых по‑прежнему напоминает скорее грабли. Ведь никому не интересно, насколько быстро работает твоя программа, если она работает некорректно. Кроме того, более жёсткий контроль со стороны компилятора заставляет переписывать «дурно пахнущий» и непереносимый код, а значит, косвенно повышает культуру программирования.

Порядок байтов при хранении чисел в памяти у «Эльбруса», в отличие от SPARC, — little endian (первым идёт младший байт), то есть как на х86. Аналогично, поскольку платформа стремится к поддержке x86-кода, отсутствуют какие-либо ограничения на выравнивание данных в памяти.

Порядок, выравнивание и переносимость

Для программистов, избалованных уютным миром Intel, может стать откровением, что за пределами этого мира обращение к памяти без выравнивания (например, запись 32‑битного значения по адресу 0×04000005) — это не просто нежелательная операция, выполняющаяся медленнее обычной, а запрещённое действие, приводящее к аппаратному исключению. Поэтому портирование номинально кросс-платформенного проекта, поначалу сводящееся к минимальным правкам, после первого же запуска может зайти в тупик, — когда становится ясно, что вся сериализация и десериализация данных (целые и вещественные числа, текст UTF‑16), рассыпанная по всему многомегабайтному коду, производится напрямую, без выделенного уровня платформенной абстракции, и в каждом конкретном случае оформлена по‑своему. Определённо, имей каждый программист возможность проверять свои нетленные шедевры на альтернативных платформах, — например, SPARC, — общемировое качество кода наверняка бы повысилось.


Более подробно об устройстве компьютеров МЦСТ на архитектурах SPARC и E2K можно прочитать в книге «Микропроцессоры и вычислительные комплексы семейства Эльбрус», которая вышла в издательстве «Питер» минимальным тиражом и давно уже разошлась по рукам, но бесплатно доступна в виде PDF (6 Мбайт) и за небольшую плату в Google Play. На фоне отсутствия другой подробной информации в открытом доступе, это издание — прямо таки кладезь знаний. Но текст сконцентрирован преимущественно на аппаратной части, алгоритмах работы буферов и конвейеров, кэшей и арифметико-логических устройств, — совершенно не затрагивается тема написания [эффективных] программ, и даже просто упоминания машинных инструкции можно по пальцам пересчитать.
Помимо компиляции высокоуровневых языков C, C++, Fortran, документация при каждом удобном случае не забывает упомянуть возможность написания программ непосредственно на Ассемблере, однако нигде не уточняется, как же именно можно приобщиться к этому филигранному искусству, — где хотя бы взять справочник по машинным командам. К счастью, в системе есть отладчик GDB, который умеет дизассемблировать код ранее скомпилированных программ. Чтобы не выходить за рамки статьи, напишем простейшую арифметическую функцию, имеющую хороший задел для распараллеливания.

uint64_t CalcParallel(
        uint64_t a,
        uint64_t b,
        uint64_t c,
        uint32_t d,
        uint32_t e,
        uint16_t f,
        uint16_t g,
        uint8_t h
) {
        return (a * b) + (c * d) - (e * f) + (g / h);
}


Вот во что она транслируется при компиляции в режиме ‑O3:

0x0000000000010490 <+0>:
        muld,1 %dr0, %dr1, %dg20
        sxt,2 6, %r3, %dg19
        getfs,3 %r6, _f32,_lts2 0x2400, %g17
        getfs,4 %r5, _lit32_ref, _lts2 0x00002400, %g18
        getfs,5 %r7, _f32,_lts3 0x200, %g16
        return %ctpr3
        setwd wsz = 0x5, nfx = 0x1
        setbp psz = 0x0
0x00000000000104c8 <+56>:
        nop 5
        muld,0 %dr2, %dg19, %dg18
        muls,3 %r4, %g18, %g17
        sdivs,5 %g17, %g16, %g16
0x00000000000104e0 <+80>:
        sxt,0 6, %g17, %dg17
        addd,1 %dg20, %dg18, %dg18
0x00000000000104f0 <+96>:
        nop 5
        subd,0 %dg18, %dg17, %dg17
0x00000000000104f8 <+104>:
        sxt,0 2, %g16, %dg16
0x0000000000010500 <+112>:
        ct %ctpr3
        ipd 3
        addd,0 %dg17, %dg16, %dr0


Первое, что бросается в глаза, — каждая команда декодируется сразу в несколько инструкций, выполняемых параллельно. Мнемоническое обозначение инструкций в целом интуитивно понятно, хотя некоторые названия кажутся непривычными после Intel: например, инструкция беззнакового расширения здесь называется sxt, а не movzx. Параметром многих вычислительных команд, помимо собственно операндов, является номер исполнительного устройства, — недаром «ELBRUS» расшифровывается как explicit basic resources utilization scheduling, то есть «явное планирование использования основных ресурсов».

Для обращения к полному 64-битному значению регистра, добавляется префикс »d»; по идее, возможно также обращение к младшим 16 и 8 битам значения. Обозначение глобальных регистров общего назначения, которых тут 32 штуки, перед номером имеет префикс »g», а локальные регистры процедур — префикс »r». Размер окна локальных регистров, запрашиваемый инструкцией setwd, может достигать 224 штуки, а откачка в стек производится автоматически по мере необходимости.

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

Впрочем, «легко читаемый код» и «эффективный код» — это далеко не одно и то же, когда дело касается машинных команд. Если компилировать без оптимизации, то код получается более последовательным и похожим на вычисление «в лоб»,  —, но ценой удлинения: вместо 6 насыщенных командных слов генерируется 8 разреженных.

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


Штатным компилятором языков C/C++ в операционной системе «Эльбрус» является LCC — собственная разработка фирмы МЦСТ, совместимая с GCC. Подробная информация о структуре и принципах работы этого компилятора не публикуется, но если верить интервью бывшего разработчика одного из нескольких развиваемых подвидов компилятора, для высокоуровневого разбора исходных кодов используется фронтэнд от Edison Design Group, а низкоуровневая трансляция в машинные команды может выполняться по‑разному — без оптимизации или с оптимизацией. Конечным пользователям поставляется именно оптимизирующий компилятор, причём не только на платформе E2K, для которой попросту не существует альтернативных генераторов машинного кода, но и на платформах семейства SPARC, где также доступен обычный GCC в составе операционной системы МСВС.

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

Из того, что всё‑таки удалось заметить невооружённым глазом при сборке программ для тестирования производительности, — LCC на E2K чаще других выдаёт предупреждения о возможных ошибках, неграмотных конструкциях или просто подозрительных местах в коде. Правда, автор не настолько хорошо знаком с GCC, чтобы гарантированно отличать уникальные сообщения LCC на русском языке от просто переведённых (причём перевод — выборочный), и не уверен, что более интенсивный поток предупреждений не является следствием автоматически выполненной конфигурации сборки. Также, не зная семантики конкретного участка кода, порой сложно понять, насколько ловок компилятор в поиске скрытых багов, или поднимает ложную тревогу. Например, в коде Postgresql одна и та же конструкция четырежды встречается в одном файле с небольшими вариациями:

for (i = 0, ptr = cont->cells; *ptr; i++, ptr++) {

        //....//

        /* is string only whitespace? */
        if ((*ptr)[strspn(*ptr, " \t")] == '\0')
                fputs("  ", fout);
        else
                html_escaped_print(*ptr, fout);

        //....//
}

Компилятор предрекает возможный выход за пределы 1‑мерного массива в строке с вызовом функции strspn. При каких обстоятельствах такое может произойти, автору не понятно (и на других платформах такого предупреждения не было, хотя режим проверок ‑Warray-bounds является стандартным для GCC), однако обращает на себя внимание многократное тиражирование одной и той же нетривиальной конструкции (раз уж понадобилось пояснять её назначение в комментарии), вместо того, чтобы вынести её в отдельную функцию с красноречивым названием, не требующим пояснений. Даже если тревога оказалась ложной, обнаружение дурно пахнущего кода — полезный эффект; этак авторы статического анализатора PVS‑Studio останутся без работы. А если серьёзно, то было бы занятно и полезно сравнить, какие дополнительные ошибки в коде действительно способен обнаруживать LCC благодаря уникальным особенностям архитектуры E2K, — заодно мир свободного программного обеспечения смог бы получить очередную порцию баг-репортов.

Ещё одним курьёзным результатом знакомства с говорливым LCC стало просвещение автора, а затем и его более опытных коллег, на тему, что такое триграфы (trigraphs) в языках C/C++, и почему они по умолчанию не поддерживаются, к счастью. Вот так живёшь и не подозреваешь, что безобидное на первый взгляд сочетание знаков пунктуации в текстовых литералах или комментариях может оказаться миной замедленного действия — или отличным материалом для программной закладки, смотря с какой стороны баррикад вы находитесь.

Неприятным следствием самостийности LCC является то, что формат его сообщений отличается от такового у GCC, и при компиляции из среды разработки (например, Qt Creator) эти сообщения попадают лишь в общий журнал работы, но не в список распознанных проблем. Возможно, это всё неким образом поддаётся настройке, — или со стороны компилятора, или в среде разработки,  —, но как минимум «из коробки» одно другого не понимает.

Традиционно остро для отечественных платформ, учитывая их сравнительно невысокую производительность, стоит вопрос кросс-компиляции, то есть сборки программ под целевую архитектуру и конкретный набор системных библиотек, используя ресурсы более мощных компьютеров, с иной архитектурой и иным программным обеспечением. Судя по опознавательным строкам в ядре системы «Эльбрус» и в самом компиляторе LCC, их сборка производится на Linux i386, но в дистрибутив самой системы этот инструментарий для х86, понятное дело, не входит. Интересно, а можно ли наоборот: на «Эльбрусе» собирать программы для других платформ? (Продвинуться дальше первой фазы сборки GCC для i386 у автора не вышло.)

Версии наиболее значимых пакетов для разработчика:

  • компиляторы: lcc 1.19.18 (gcc 4.4.0 compatible);
  • интерпретаторы: erlang 15.b.1, gawk 4.0.2, lua 5.1.4, openjdk 1.6.0_27 (jvm 20.0‑b12), perl 5.16.3, php 5.4.11, python 2.7.3, slang 2.2.4, tcl 8.6.1;
  • средства сборки: autoconf 2.69, automake 1.13.1, cmake 2.8.10.2, distcc 3.1, m4 1.4.16, make 3.81, makedepend 1.0.4, pkgtools 13.1, pmake 1.45;
  • средства компоновки: binutils 2.23.1, elfutils 0.153, patchelf 0.6;
  • фреймворки: boost 1.53.0, qt 4.8.4, qt 5.2.1;
  • библиотеки: expat 2.1.0, ffi 3.0.10, gettext 0.18.2, glib 2.36.3, glibc 2.16.0, gmp 4.3.1, gtk+ 2.24.17, mesa 10.0.4, ncurses 5.9, opencv 2.4.8, pcap 1.3.0, popt 1.7, protobuf 2.4.1, sdl 1.2.13, sqlite 3.6.13, tk 8.6.0, usb 1.0.9, wxgtk 2.8.12, xml‑parser 2.41, zlib 1.2.7;
  • средства тестирования и отладки: cppunit 1.12.1, dprof 1.3, gdb 7.2, perf 3.5.7;
  • среды разработки: anjuta 2.32.1.1, glade 2.12.0, glade 3.5.1, qt‑creator 2.7.1;
  • системы контроля версий: bzr 2.2.4, cvs 1.11.22, git 1.8.0, patch 2.7, subversion 1.7.7.


Опять же, если вы ожидали GCC 5, PHP 7 и Java 9, то это — ваши проблемы, как говорит один известный футболист. В данном случае надо ещё сказать спасибо, что хотя бы не GCC 3.4.6 (LCC 1.16.12), как в составе прежних версий системы «Эльбрус», или GCC 3.3.6 в составе МСВС 3.0; кстати, основным компилятором в МСВС 3.0 и поныне является GCC 2.95.4 (а чему удивляться, когда там ядро из 2.4-ветки?). По сравнению с прежней ситуацией, когда можно было наткнуться на баг GCC, исправленный в апстриме ещё десять лет назад, в новой системе почти райские условия, — можно даже на С++11 замахнуться, если не требуется сохранять обратную совместимость.

Появление OpenJDK хоть в каком-то виде уже можно назвать большим прорывом, — ведь нелюбовь к Java и Mono в подобных системах давно известна; и нелюбовь эту можно понять, когда даже нативные программы едва шевелятся. Поскольку среди коллег автора есть много джавистов, в силу вышеперечисленных обстоятельств вынужденных сдерживать души прекрасные порывы, было решено отдельную серию тестов производительности посвятить Java. Забегая вперёд, отметим, что результаты оказались обескураживающими даже в относительном выражении: с таким же успехом можно писать интерпретируемые скрипты на PHP или Python, наверное.

Поддержкой одних только C и C++ заявленная совместимость с GNU Compiler Collection не ограничивается: в системе ещё есть транслятор Фортрана. Поскольку автор знаком разве что с профессором Фортраном, всем интересующимся можно порекомендовать декабрьский топик на «Сделано у нас», где в комментариях затрагивается тема использования этого языка в качестве бенчмарка.

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

© Geektimes