Ваш компьютер не является быстрой версией PDP-11


Привет, Хабр!

Меня зовут Антон Довгаль, я С (и не только) разработчик в Badoo.

Мне попалась на глаза статья Дэвида Чизнэлла, исследователя Кембриджского университета, в которой он оспаривает общепринятое суждение о том, что С — язык низкого уровня, и его аргументы мне показались достаточно интересными.

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

Производители процессоров не одиноки в этом. Разработчики компиляторов C/C++ тоже внесли свою лепту.

Что такое язык низкого уровня?


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

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

Чтобы быть «ближе к железу», язык должен предоставлять абстракции, которые соответствуют абстракциям целевой платформы. Легко доказать, что С был низкоуровневым языком в PDP-11. Последовательное выполнение программ, плоское адресное пространство, даже операторы пре- и постинкремента отлично ложились на режимы адресации PDP-11.

Быстрые эмуляторы PDP-11


Ключевая причина появления уязвимостей Spectre и Meltdown в том, что создатели процессоров не просто делали быстрые процессоры, а делали быстрые процессоры с интерфейсом PDP-11. Это важно, потому что позволяет программистам на С и дальше верить в то, что их язык близок к аппаратной части.

Код С предоставляет в основном последовательный абстрактный автомат (до C11 — полностью последовательный, если исключить нестандартные расширения). Создание нового потока — это вызов функции библиотеки, операция, которая довольно дорога. Поэтому процессоры, желая продолжать выполнять код C, полагаются на параллелизм на уровне команд (instruction-level parallelism, ILP). Они анализируют соседние операции и выполняют независимые параллельно. Это значительно усложняет процессоры и приводит к увеличению потребления энергии, но позволяет программистам писать по большей части последовательный код. В противоположность этому графические процессоры (GPU) достигают высокой производительности другим путём: они требуют написания параллельных программ.

Высокий параллелизм на уровне команд является прямой причиной появления Spectre и Meltdown. Современный процессор Intel выполняет до 180 инструкций одновременно (в отличие от последовательной абстрактной машины C, которая ожидает, что предыдущая инструкция выполнится перед тем, как начнётся выполнение следующей). Типичная эвристика кода на С показывает, что есть одно ветвление в среднем на каждые семь инструкций. Если вы хотите держать конвейер инструкций полным, то вам нужно угадать следующие 25 ветвей. Это, в свою очередь, добавляет сложности — неправильно угаданную ветвь процессор сначала просчитает, а потом результаты расчётов выбросит, что негативно влияет на энергопотребление. Эти выброшенные данные имеют видимые косвенные результаты, что и было использовано в атаках Spectre и Meltdown.

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

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

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

Оптимизация С


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

К сожалению, с помощью простой трансляции нельзя получить быстрый код из С.
Архитекторы процессоров прикладывают героические усилия для создания чипов, которые могут выполнять код на С быстро. Но уровни быстродействия, которые ожидают увидеть программисты, достигаются только с помощью невероятно сложных оптимизаций, выполняемых компилятором.
Компилятор Clang (включая соответствующие части LLVM) — это около 2 млн строк кода. Для анализа и трансформации кода, которые необходимы для ускорения С, нужны около 200 000 строк кода (без учёта комментариев и пустых строк).

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

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

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

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

Рассмотрим две базовые оптимизации, которые производит компилятор C: SROA (scalar replacement of aggregates, скалярная замена агрегатов) и размыкание цикла.

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

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

В С неинициализированная переменная имеет неопределённое значение, которое может быть разным при каждом обращении. Это важно, поскольку позволяет реализовать ленивую переработку (lazy recycling) страниц памяти. Например, во FreeBSD реализация malloc() сообщает системе, что страницы более не задействованы, а система использует первую запись в страницу как доказательство того, что это не так. Обращение к только что выделенной памяти может получить старое значение, тогда операционная система может повторно использовать страницу памяти, после чего заменить её на заполненную нулями страницу при следующей записи в другое место страницы. Второе обращение к тому же месту страницы получит нулевое значение.

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

В итоге можно заставить код на C работать быстро, но только затратив тысячи человеко-лет на создание достаточно умного компилятора. Но и это возможно только при условии нарушения некоторых правил языка. Создатели компиляторов позволяют программистам на C представлять, что они пишут код, который «близок к железу», но им приходится генерировать машинный код, который ведёт себя по-другому, чтобы программисты продолжали верить в то, что они пишут на быстром языке.

Понимая C


Одним из базовых атрибутов языка низкого уровня является то, что программисты могут легко понять, как абстрактная машина языка переносится на физическую машину. Это определённо было так на PDP-11, где выражения на C транслировались в одну или две инструкции. Аналогично компилятор клал переменные в слоты стека и преобразовывал простые типы в понятные для PDP-11.

С тех пор реализации C стали гораздо сложнее — для поддержания иллюзии того, что C легко переносится на аппаратную платформу и работает быстро. В 2015 году результаты опроса среди программистов на C, авторов компиляторов и членов комитета по стандартизации показали, что существуют проблемы с пониманием C. Например, этот язык позволяет реализации добавлять выравнивание в структуры (но не в массивы), чтобы гарантировать, что все поля правильно выровнены для целевой платформы. Если вы заполните эту структуру нулями и потом укажете значение некоторым полям, будут ли в битах выравнивания нули? Согласно результатам опроса, 36% были уверены, что будут, а 29% опрошенных не знали ответа. В зависимости от компилятора и уровня оптимизации это может быть правдой (или нет).

Это довольно тривиальный пример, однако многие программисты или дают неправильный ответ, или не могут ответить вовсе.

Если добавить указатели, семантика C становится ещё более запутанной. Модель BCPL была довольно проста: все значения являются словами. Каждое слово — это или данные, или адрес в памяти. Память — это плоский массив ячеек с индексацией по адресу.

Модель С позволяет реализацию для разных платформ, включая сегментированные архитектуры, где указатель может состоять из ID сегмента и смещения, а также виртуальные машины со сборщиком мусора. Спецификация C ограничивает разрешённые операции с указателями, чтобы избежать проблем с такими системами. Ответ на Defect Report 260 содержит упоминание происхождения указателя:
«Реализации могут следить за происхождением набора битов и обращаться с содержащими неопределённое значение иначе, чем с теми, которые содержат определённое. Они могут обращаться с указателями по-разному в зависимости от их происхождения, даже если они одинаковы с точки зрения их битового значения».

К сожалению, слово «происхождение» отсутствует в спецификации C11, поэтому компиляторы сами решают, что оно означает. GCC и Clang, например, отличаются в том, сохраняет ли своё происхождение указатель, который конвертировали в целое и назад. Компиляторы могут решить, что два указателя на результаты malloc() при сравнении всегда дают отрицательный результат, даже если они указывают на один и тот же адрес.

Эти недопонимания не являются сугубо академическими. Например, уже наблюдались уязвимости, которые стали результатом переполнения знакового целого (неопределённое поведение в C) или разыменования указателя до его проверки на NULL, притом что компилятору было указано, что указатель не может быть NULL.

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

Представляя процессор не для C


Предлагаемые исправления для защиты от Spectre и Meltdown вызывают серьёзное ухудшение производительности, сводя на нет все достижения микроархитектуры за последнее десятилетие. Возможно, пора перестать думать о том, как сделать код на C быстрее, и вместо этого задуматься о новых моделях программирования на процессорах, которые созданы для скорости.

Есть множество примеров архитектур, которые не были сфокусированы на традиционном коде C и из которых можно черпать вдохновение. Например, такие ориентированные на многопоточность процессоры, как Sun/Oracle UltraSPARC Tx, не требуют столько кеша, чтобы держать занятыми свои исполнительные устройства. Исследовательские процессоры расширили этот концепт до очень большого количества аппаратно-планируемых потоков. Ключевая идея состоит в том, что с достаточным количеством потоков процессор может приостановить те потоки, которые ожидают данных, и наполнить исполнительные устройства инструкциями из других потоков. Проблема же состоит в том, что программы на C обычно имеют очень мало потоков.

ARM’s SVE (Scalar Vector Extensions, скалярные векторные расширения) — ещё одна аналогичная работа из Беркли, которая предлагает взглянуть на улучшенный интерфейс между программой и аппаратным обеспечением. Обычные блоки векторизации реализуют операции с векторами фиксированного размера и ожидают, что компилятор адаптирует алгоритм к указанному размеру. В противоположность этому интерфейс SVE предлагает программисту самостоятельно описать уровень параллелизма и ожидает, что аппаратная часть самостоятельно адаптирует его к имеющимся в наличии исполнительным устройствам. Использовать это в C сложно, потому что автовекторизатор должен рассчитать параллелизм на основании циклов в коде.

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

Неизменяемые объекты могут упростить кеши ещё больше, а также сделать некоторые операции дешевле. В проекте Maxwell от Sun Labs было отмечено, что объекты в кеше и недавно созданные объекты  — почти всегда одни и те же. Если объекты умирают до того, как их исключают из кеша, то можно не записывать их в основную память и таким образом сэкономить потребление энергии. Проект Maxwell предложил сборщик мусора, который работал в кеше и позволял быстро перерабатывать память. С неизменяемыми объектами в куче и изменяемым стеком сборщик мусора становится очень простым конечным автоматом, который легко реализуется в аппаратной части и позволяет эффективно использовать относительно небольшой кеш.

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

В сфере разработки программного обеспечения есть миф о том, что параллельное программирование — это сложно. Алан Кэй был бы очень удивлён, услышав это: он научил детей использовать модель акторов, с помощью которой они писали программы на более чем 200 потоков. Это неизвестно и программистам на Erlang, которые часто пишут программы с тысячами параллельных компонентов. Более правильно говорить, что параллельно программирование сложно на языке с абстрактной машиной, подобной C. И если обратить внимание на преобладание параллельного аппаратного обеспечения (от многоядерных процессоров до многоядерных GPU), то это просто ещё один способ сказать, что C не подходит для современного аппаратного обеспечения.

Комментарии (244)


  1. apro
    20.08.2018 18:25
    +3

    Современный процессор Intel выполняет до 180 инструкций одновременно (в отличие от
    последовательной абстрактной машины C

    А точно эта и куча перечисленных сложностей из-за С?
    Мне всегда казалось, что из-за обратной совместимости "ассемблера" (если мы уж говорим о языках программирования). Иными словами уже написанные и скомпилированные программы
    нужно с каждой ревизией процессора выполнять все быстрее и быстрее. А никак не для того,
    чтобы облегчить разработчикам С компилятора жизнь.


    1. khim
      21.08.2018 00:39

      Отчасти да, отчасти нет. Библиотеки на ARM'е (на сотовых) — обычно перекомпилированные версии x86 библиотек… но они всё равно написаны на C.


      1. Dima_Sharihin
        21.08.2018 16:10

        Числодробилки часто оптимизируют под VFP и NEON, так что там тоже уже платформо-зависимый код на асме или интринсиках


        1. khim
          21.08.2018 16:29

          Так всегда было. И всегда этого «платформно-зависимого кода» было несколько процентов.


  1. robert_ayrapetyan
    20.08.2018 18:50
    +11

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


    1. arteast
      20.08.2018 19:13
      +8

      Ассемблер в x86 — это тоже высокоуровневый язык. Автор оригинальной статьи нацеливается на C (один из двух-трех языков, которые дожили от 70х до наших времен, и наверняка самый из них популярный), но большинство аргументов применимы ко всем языкам подобной идеологии, включая даже ассемблер и машинный код. В современном x86 процессоре есть встроенный оптимизирующий компилятор, который преобразует "высокоуровневый" ассемблер во внутренний низкоуровневый — включая SSA (переименование регистров) и параллелизацию. Посыл статьи (если отбросить C-специфичные вещи про выравнивание, неопределенные биты и тд) в том и был, что C (и все другие похожие языки) был простым языком, который напрямую ложился на реальное железо, а теперь инженеры пытаются всячески вбить новое железо (которое имеет принципиально другие допущения!) в видение мира старого языка (а разработчики компиляторов в то же время пытаются как-то приспособиться к новому железу, которое пытается выглядеть старым). Строго та же фигня происходит и с, к примеру, архитектурой ОС — до сих пор программы притворяются, что весь компьютер принадлежит им, а ОС всячески им в этом потворствует ценой приличных накладных расходов. Обратная совместимость — главное зло мира...


      1. robert_ayrapetyan
        20.08.2018 19:55
        +2

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


        1. arteast
          20.08.2018 21:09

          Правильно, нечего их делить, но делят же. Тут вот ниже про микроконтроллеры упомянули правильно — они примерно соответствуют тем машинам, для которых разрабатывался C. В прицеле на них C будет низкоуровневым, а Java — не очень. В прицеле x86/ARM/POWER — какой-то большой разницы между С и Java уже нет.
          Про фортран -он такой же, как и C, просто автор еще и выискивает в C любые вещи, которые мешают компилятору делать переход 1-1 из исходного кода в машинный (как то выравнивание и алиасинг). Эрланг же был упомянут как пример принципиально другой идеологии (многопоточного) программирования, которая может казаться странной и сложной для приверженца С-подобных языков и POSIX-подобной share-everything многозадачности, но современное железо может оказаться больше заточенным под модель эрланга, чем под модель C.
          Большая часть этой статьи — это вопль в пустоту о том, что если делать все с нуля — включая парадигму программирования, которая учитывает текущие реалии в железе — то можно сделать процессор значительно проще, дешевле, холоднее и быстрее тех, что есть сейчас. Но такой процессор не сделают, потому что программы на C на нем будут работать медленно.


          1. Siemargl
            20.08.2018 22:49

            Просто нет языков под специализацию процессора. Вот была статья про Мультиклет — на который С-парадигма нормально не ложится. Но не ложится и ничего более из языков.

            Еще и мир устроен лентяями — стоит попробовать спросить J-скриптера, во сколько тактов развернется его лямбда?


            1. Goodkat
              20.08.2018 23:29

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


              1. Siemargl
                20.08.2018 23:37

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

                Иначе язык отдает это на откуп компилятору, который ...., дом который построил Джек


                1. 0xd34df00d
                  21.08.2018 03:08
                  +1

                  Этого недостаточно. Нужно еще знать, где параллелизм оправдан, а где — нет, и какой гранулярности.


              1. mayorovp
                21.08.2018 09:43
                +1

                Только не в JS. В этом языке функция map явно однопоточна: стандарт предписывает вызывать переданную функцию строго последовательно...


            1. firedragon
              21.08.2018 00:13
              -1

              Если что то тормозит, запускай профайлер.
              Кстати сегодня он мне сэкономил тучу времени. Получать культуру в цикле было плохой идеей.
              var ci = (CultureInfo)CultureInfo.CurrentCulture.Clone();


            1. 0xd34df00d
              21.08.2018 03:07

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


              1. TheIseAse
                21.08.2018 16:08

                Статью прочитал, а комментарии почти нет. Вопрос к вам и к знающим людям: принимая во внимание все вышесказанное, можно ли сделать какой-нибудь функциональный язык, который будет решать задачи принципиально быстрее, чем С, и будет в каком-то смысле низкоуровневым? Имеются в виду как бенчмарковские задачи вроде сортировок, так и реальные задачи вроде HTTP-сервера.

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


                1. 0xd34df00d
                  21.08.2018 17:00

                  Смотря как определено «быстрее».

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

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

                  А что до разницы в производительности ­— ну, это зависит. У меня в практике встречались задачи, где хаскель уделывал С++ (с учётом замечания выше о ресурсах), и встречались задачи, где С++ уделывал хаскель на несколько порядков. В последнем случае просто было очень много данных и очень много последовательных циклов по ним.


        1. slovak
          20.08.2018 23:52

          Существуют расширения для С (CUDA, например) которые умеют работать с различными уровнями памяти на GPU — shared, local, global. Что в принципе схоже с организацией кешей в x86 и x64.


          1. avdx
            21.08.2018 19:01

            На самом деле задач, где имеет смысл использование shared памяти на gpu не так уж много. Nvidia в свое время сделало кэширование global памяти, такое же как в обычных процессорах, но потом в один прекрасный момент молча это выпилили. В результате производительность в конкретной задаче конкретно просела. В замен они добавили специальную инструкцию, для чтения глобальной памяти, которая гарантировала попадание данных в кэш. Но только это работало для константных данных и следить за тем, чтобы они не менялись должен был сам разработчик.


            1. slovak
              21.08.2018 19:49

              задач, где имеет смысл использование shared памяти на gpu не так уж много
              Расскажите, пожалуйста, по-подробнее с этого места. Какого типа операции или задачи над данными на GPU не имеет смысла выполнять с использованием shared memory? И какие имеет смысл?


              1. avdx
                21.08.2018 21:43
                +1

                По моему опыту, не имеет смысл использовать shared memory в большинстве задач, потому что просто нечего там хранить: прочитал элемент из global или текстуры, обработал, сохранил туда же или в другой буфер.
                Классическим примером, где нужно использование shared, является перемножение матриц, там это дает большое ускорение, но это весьма специфический пример.
                В своих задачах я использую shared memory в основном при всякой редукции, тут она нужна чисто для обмена данными между потоками в блоке. Но для этого в той же CUDA есть и другие механизмы, можно обойтись и без нее.
                Еще shared memory нужна, когда каждому потоку нужен какой то локальный буфер для своих вычислений. Но здесь размер буфера получается весьма ограничен, да и честно сказать это не очень эффективно. Если в задаче требуется буфер, то обычно ее достаточно сложно реализовать на gpu.
                Больше что-то ничего не приходит в голову, для чего может shared memory понадобиться. Скорее в остальном это специфические задачи.


                1. slovak
                  22.08.2018 00:54

                  А вот у меня обратный случай — большинство задач связано с некоторым состоянием, которое претерпевает множество изменнеий в процессе работы приложения. При этом это состояние выгодно разбить на кусочки и хранить в shared memory. При этом если размер состояния не помещается в shared — то практически всегда есть возможность реализации ручного кеширования части этого состояния.
                  >> Скорее в остальном это специфические задачи.
                  Да.


      1. int33h
        20.08.2018 19:58
        -4

        Обратную совместимость я бы лучше заменил банальным словом «лень»…
        Обратная совместимость — продукт нежелания миллионов программистов переписывать миллиарды строк кода(знаменитое Java-вское «написано однажды — работает всегда»). Каждому из нас не хочется этого делать… Но проблема тут не в программисте, а в языке, на котором написан код.
        Язык С, как и все его компилируемые наследники(C++, Java, ..., но не Python, хотя там свои проблемы) в нынешние дни имеют ряд коренных недостатков, которые были не видны людям из далеких 70-x. Например, в С/C++ нельзя просто взять и написать ассемблерные вставки или функции. Это просто отсутствует в стандарте. И что прикажете с этим делать? А ведь даже такая малая стандартизируемая возможность в корне может ускорить выполнение кода…


        1. picul
          20.08.2018 21:04
          +3

          Обратную совместимость я бы лучше заменил банальным словом «лень»
          А когда этим заниматься миллионам программистов, если им надо работать по 8+ часов в день, что бы семью кормить?
          Но проблема тут не в программисте, а в языке
          Нет, проблема в суровой реальности — людишкам, которые не бум-бум в компуктерах, нужны новые интернет-магазинчики, а не стоп-кадр на 10 лет, что бы переделать все по-нормальному.
          Например, в С/C++ нельзя просто взять и написать ассемблерные вставки или функции.
          Можно практически во всех популярных реализациях.
          А ведь даже такая малая стандартизируемая возможность в корне может ускорить выполнение кода…
          В 0.001% случаев, потому что компиляторы уже давно умнее нас.


          1. 0xd34df00d
            21.08.2018 03:11
            +1

            Можно практически во всех популярных реализациях.

            Да, но в Стандарте понятие ассемблерных вставок, ассемблера вообще и прочих FFI не описано.

            Но это в каком-то смысле буквоедство.


            1. AntonSazonov
              21.08.2018 11:52
              +2

              Да, но в Стандарте понятие ассемблерных вставок, ассемблера вообще и прочих FFI не описано.

              Ну как же. Раздел 9.9 как раз и описывает понятие ассемблерной вставки:image


              1. 0xd34df00d
                21.08.2018 16:43

                Нет, понятие ассемблерной вставки он не описывает (по крайней мере, в обычном человеческом понимании ассемблера). Он описывает ключевое слово и контекст в грамматике (ну, я там ниже про это писал).

                Если в вашей реализации string-literal будет кодом на, я не знаю, хаскеле, то это всё равно будет стандартом считаться ассемблерной вставкой. Какой-то намёк на inline assembly там только в примечании, но оно никого ни к чему не обязывает.


                1. AntonSazonov
                  21.08.2018 17:42

                  Вы путаете значение слова "понятие" с чем-то другим.


          1. Druu
            21.08.2018 09:14

            deleted


          1. Source
            21.08.2018 17:06

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

            Ну, может и без стоп-кадра возможно… Например, начать с серверов… Сделать ЯП мимо C, под современный проц, и переписать на нём BEAM (виртуальную машину Erlang). Кто бы это только профинансировал?..


            1. berez
              22.08.2018 10:28
              +1

              Для начала надо будет переписать на новом ЯП операционную систему — ибо она точно написана на С. Иначе все это полумеры и косметика над трупом. :)


        1. AntonSazonov
          20.08.2018 23:10
          +2

          Например, в С/C++ нельзя просто взять и написать ассемблерные вставки или функции. Это просто отсутствует в стандарте.
          Стандарт описывает и разрешает ассемблерные вставки. Единственное что он не описывает, так это синтаксис внутри самой вставки.


          1. 0xd34df00d
            21.08.2018 03:13

            Стандарт по большому счёту резервирует ключевое слово asm и кусок грамматики вокруг него. Не описывает он не только синтаксис, но и семантику: необходимо ли иметь доступ к локальным/глобальным переменным, необходимо ли указывать, что какой-то регистр портится/не портится вставкой, и так далее.


            1. AntonSazonov
              21.08.2018 11:52
              +1

              Да. Я в курсе. Но речь шла не об этом. Речь была о том, что:

              … Это просто отсутствует в стандарте. ...

              Как сказали выше, каждый современный компилятор подерживает вставки по-своему. Документации об этом навалом в свободном доступе.


        1. Druu
          21.08.2018 09:20
          +1

          Обратная совместимость — продукт нежелания миллионов программистов переписывать миллиарды строк кода(знаменитое Java-вское «написано однажды — работает всегда»).

          Да не, у программистов никакого нежелания нет. платите деньги — и вам все перепишут.


      1. Lsh
        20.08.2018 23:08

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

        А как надо?


        1. Goodkat
          20.08.2018 23:54

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


          1. arteast
            21.08.2018 03:37

            Я имел в виду и саму вытесняющую многозадачность с внезапным прерыванием выполнения и сбросом контекста выполнения куда-то в память, и раздельные адресные пространства для приложений, которые влекут за собой еще большие накладные расходы на переключение, и переходы в ядро и обратно, и прочие "особенности" привычных нам ОС.
            Как надо? Не знаю, я сам заядлый C++ программист ) Но подозреваю, что куда-то в сторону managed языков и кооперативного шедулинга мелких задач/вытесняющей многозадачности, но не в любой точке кода, а в определенных точках останова (VM может гарантировать, что одна программа не перетрет другую и не захапает себе все время)


            1. novice2001
              21.08.2018 16:51

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


              1. arteast
                22.08.2018 03:14

                Встречное предложение — оцените ресурсы, потраченные на борьбу с имеющимися ОС. Сколько человеко-лет ушло на написание TBB и всех ему подобных диспетчеров (включая что-то похожее в каждом первом игровом движке), которые суть подменяют встроенный в ОС диспетчер потоков на свой диспетчер задач — потому что встроенный механизм переключения потоков слишком накладный? Сколько ушло на разработку lockless структур — потому что локи/спинлоки использовать нельзя, потому что а вдруг вытеснят в критической секции?


                1. novice2001
                  22.08.2018 08:09

                  Когда вы пишете «слишком накладный» и не пишете, что именно это значит, то это совершенно бессмысленное выражение. То ли это 0,1%, то ли 99%. Кому-то суп жидок, кому-то жемчуг мелок.
                  Неважно сколько человеко-лет ушло на написание того, что вы перечислили. Важно, что априори оно много меньше того, сколько человеко-лет понадобится на реализацию ваших предложений.
                  Когда действительно большому количеству пользователей (в данном случае программистов) что-то становится действительно необходимым, то появляется библиотека, фреймворк и т.п. Этот ваш intra-app «диспетчер задач» по сути реализация user-level threads, давно известной концепции. Раз нет одной или нескольких популярных реализаций такой концепции, следовательно это проблемы явного меньшинства, которое и вынуждено строить свои велосипеды. А менять ВСЁ ради решения проблем меньшинства как-то нерационально.


                  1. arteast
                    22.08.2018 13:22

                    Реальный пример — наивная реализация flow based programming, где каждая нода — это поток, обмен друг с другом сообщениями через очереди. Делаем несколько тысяч нод-потоков, запускаем на большой машине с кучей ядер, набрасываем миллионы сообщений в секунду и получаем >50% накладных расходов чисто на переключение потоков.
                    Про явное меньшинство — TBB, Cilk, часть OpenMP, Microsoft's PPL/Concurrency, встроенный в OS X GCD, встроенный в Go goroutines, встроенные в Erlang процессы, TAP в .NET, десятки самописных для разных приложений и библиотек. Как только мы переходим от use case "несколько долгоживущих независимых потоков, которые долго-долго долбят числа или висят в I/O" — т.е. однопоточной программы x N, к параллельному решению относительно маленьких задач, как абстракция "бесконечного количества потоков" становится нежизнеспособной. Все эти "библиотеки и фреймворки" позволяют обойти накладные расходы от ОС путем отказа от этой абстракции; вместо этого создается один или несколько потоков (но не больше количества процессоров!), иногда они еще прибиваются жестко к процессорным ядрам, и все задачи кооперативно выполняются в них.
                    Интересно, что любой чисто асинхронный код естественным образом порождает задачи, а не потоки; если не ошибаюсь, то, например, все UWP программы такие.


                    1. novice2001
                      22.08.2018 13:33
                      +2

                      Отлично, все вышеперечисленное как раз подтверждает мою точку зрения. Там где такая архитектура нужна — она УЖЕ есть, и не требует переделки всех систем в мире. Кому-то нужны миллионы микроскопических задач, а кому-то единицы-десятки крупных. Полностью переделывать дизайн ОС из-за этого просто глупо.


                      1. arteast
                        22.08.2018 17:35

                        Да-да, все так и есть. Никто не будет делать новые ОС по другим принципам, раз уже есть старые и привычные. Никто не будет делать новые процессоры по другим принципам, раз уже есть старые и привычные. Пусть неоптимальные, но мы к ним привыкли, кода понаписали… Я уже говорил про обратную совместимость, и вы подтверждаете мою точку зрения ))
                        На самом деле иногда пытаются — Itanium должен был давать лучший доступ до "настоящего" железа, чем текущие лидеры, Project Singularity пытался избавиться от накладных расходов на изоляцию ядра ОС — увы, они не выжили.


                1. DistortNeo
                  22.08.2018 14:06
                  +1

                  Сколько человеко-лет ушло на написание TBB и всех ему подобных диспетчеров (включая что-то похожее в каждом первом игровом движке)

                  У меня — пара дней, причём я ещё получил до кучи выигрыш в виде сокращения накладных расходов по сравнению с TBB: универсальные средства не являются оптимальными для каких-то определённых задач.


            1. mayorovp
              22.08.2018 10:59
              +1

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

              Языковые VM тут ничего принципиально не меняют: с точки зрения пользовательского кода, работающего в песочнице, многозадачность всегда будет вытесняющей. Независимо от того кто это самое вытеснение будет обеспечивать — ОС или среда исполнения.


              1. qw1
                22.08.2018 12:51

                Раздельные адресные пространства необязательны (.NET может запускать недоверенный код в общем адресном пространстве).

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

                Также, ради оптимизации, jit-компилятор может разбивать цикл по 1e6 итераций на 1e2 циклов по 1e4 итераций и между ними ставить yield, что минимально затронет производительность.


                1. novice2001
                  22.08.2018 13:42

                  1. В каком «общем» адресном пространстве??? Адресное пространство процессов всегда изолировано.
                  И речь не о том, что общее адресное пространство невозможно, а о том, что оно небезопасно. Поэтому если есть недоверенный код (а он есть всегда), то изоляция адресных пространств обязательна.
                  2. В чем смысл передачи функций ОС компилятору? И когда появится универсальный компилятор для любого языка?


                  1. qw1
                    22.08.2018 14:27
                    +2

                    Разные AppDomains могут находиться в одном процессе и тем не менее, быть изолированными.

                    В чем смысл передачи функций ОС компилятору?
                    А в чем смысл идеи VLIW, отдать шедулинг команд компилятору, когда и процессор неплохо справляется. Моё замечание о том, что такая реализация в принципе возможна. Может, с какими-то дополнительными факторами она станет выгодной.


                    1. novice2001
                      22.08.2018 15:30
                      +1

                      1. Ключевой момент в том, что в одном процессе. Разные процессы обязательно должны быть изолированными.
                      2. Среди процессоров общего назначения VLIW-разновидностей как-то не очень много. Все-таки это скорее для специализированных процессоров.
                      В то же время вопрос зачем отдавать scheduling компилятору не лишен смысла и для «обычных» процессоров. «Окно», в пределах которого способен манипулировать исполнением процессор, минимально. В то же время компилятор может делать это в масштабах всей программы, к тому же это снижает сложность, а соответственно, стоимость и энергопотребление процессора.
                      Но одно дело переносить функции железа на софт, и совсем другое функции софта на тот же софт. Причем ваш вариант однозначно ухудшает результат и вот почему.
                      В случае с переносом планирования инструкций из процессора в компилятор мы один раз оптимизируем и неограниченное количество раз исполняем уже оптимизированный двоичный код. Миллиарды процессоров экономят при этом буквально гигаватты мощности.
                      А в случае с переносом вытесняющей многозадачности из ОС, которая исполняет готовый машинный код, в VM, которая исполняет байт-код, мы, наоборот, получаем миллиарды лишних компиляций. И, поскольку такая jit-компиляция должна быть достаточно быстрой, то она, однозначно, будет хуже, чем традиционная aot-компиляция. Т.е. мы будем тратить лишние гигаватты и на саму компиляцию и на хуже оптимизированный код.
                      И вы не убедите меня, что современные jit-компиляторы очень быстрые и выдают отличный машинный код, пока я наблюдаю ужасные тормоза на многих сайтах, завязанных на js.


                      1. DistortNeo
                        22.08.2018 16:07
                        +2

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

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


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


                        https://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array


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

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


                        1. novice2001
                          22.08.2018 16:19

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

                          Мы его уже получаем.

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

                          Предсказание переходов — это не оптимизация кода.


                          1. khim
                            22.08.2018 16:56
                            +1

                            Вообще-то предсказание переходов — то с чего начинается подавляющее большинство оптимизаций.

                            Скажем вынесение констант из цикла, обычно увеличивает работу вне цикла и уменьшает внутри. Чтобы понять — стоит ли овчинка выделки нужно знать сколько раз цикл будет исполняться.

                            То же самое — встраивание функций. И далее везде.


                            1. novice2001
                              22.08.2018 18:01
                              +1

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


                              1. tyomitch
                                22.08.2018 18:11

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


                                1. novice2001
                                  22.08.2018 18:16
                                  -2

                                  Компилятор не занимается предсказанием переходов. Он занимается генерацией такого кода, который облегчает работу предсказателя переходов в процессоре.
                                  Даже с точки зрения языка невозможно считать, что кто-то занимается «предсказанием», если этот кто-то «знает».
                                  Я читал Intel® 64 and IA-32 Architectures Optimization Reference Manual, да.


                                  1. tyomitch
                                    22.08.2018 18:21
                                    +1

                                    kristerw.blogspot.com/2017/02/branch-prediction.html

                                    The predictors are defined in predict.def (some of the definitions seem reversed due to how the rules are implemented, e.g. PROB_VERY_LIKELY may mean “very unlikely”, but the comments describing each heuristic are correct). You can see how GCC is estimating the branch probabilities by passing -fdump-tree-profile_estimate to the compiler, which writes a file containing the output from the predictors for each basic block

                                    Predictions for bb 2
                                      DS theory heuristics: 1.7%
                                      combined heuristics: 1.7%
                                      pointer (on trees) heuristics of edge 2->4: 30.0%
                                      call heuristics of edge 2->3: 33.0%
                                      negative return heuristics of edge 2->4: 2.0%
                                    

                                    as well as (when using GCC 7.x) the IR annotated with the estimated probabilities.


                                    1. Druu
                                      22.08.2018 18:25

                                      Не, ну формально он прав, это не предсказание переходов (в том смысле в котором их предсказывает процессор — для конкретного перехода), это оценивание вероятностей переходов.


                                    1. novice2001
                                      22.08.2018 20:50

                                      Меня слегка опередил комрад Druu.
                                      Именно это я и имел в виду.


                                  1. DistortNeo
                                    22.08.2018 18:27

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


                                    1. novice2001
                                      22.08.2018 21:00

                                      Нет, просто вы слегка упустили контекст.
                                      Я лишь говорил, что перенос значительной степени оптимизации в компилятор из железа имеет смысл не только для VLIW (которые мой оппонент привел как пример), но и для более популярных архитектур, таких как x86/x64.
                                      Но это было сказано не потому, что я горячий сторонник такого переноса самого по себе, а как пример именно оправданности в противовес неоправданности переноса функций ОС к ВМ.
                                      Видимо из-за этого вы и начали со мной спорить, хотя, судя по другим комментариям, вполне разделяете мое мнение о явном преимуществе качества aot-компиляции перед jit-компиляцией, например.


                                      1. DistortNeo
                                        22.08.2018 22:21

                                        Вполне разделяете мое мнение о явном преимуществе качества aot-компиляции перед jit-компиляцией, например.

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


                                        И я вообще не рассматриваю архитектуру x86/x64 как удачную. За ней тянется ужасный шлейф обратной совместимости.


                                        1. khim
                                          23.08.2018 00:12

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

                                          Потому Top500 и выглядит так, как он выглядит…


                          1. DistortNeo
                            22.08.2018 17:44

                            Мы его уже получаем.

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


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


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


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


                            Предсказание переходов — это не оптимизация кода.

                            Ничего себе заявление. А profile-guided optimization — это что, по-вашему?


                            1. Gryphon88
                              22.08.2018 17:53

                              рограммы придётся распространять не в виде бинарников
                              Возможно, маркетинг просто перейдёт от загрузки/поставки носителя к on-demand compilation: пользователь скачивает аналог CPU-Z, отправляет отчёт, платит, скачивает программу, скомпилированную под данную конкретную конфигурацию систему.


                              1. DistortNeo
                                22.08.2018 18:01

                                Сменил систему — переклмпилируй всё заново.


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


                                1. Gryphon88
                                  22.08.2018 18:17
                                  +1

                                  Сменил систему — перекомпилируй всё заново.
                                  И плати повторно, или оплачивай поддержку. Как вариант — дефальтная компиляция, универсальная, но неоптимальная.
                                  ещё большие гигаватты электроэнергии будут тратиться
                                  Когда это кого останавливало, если отбивается по деньгам?


                                  1. khim
                                    22.08.2018 19:16

                                    Дык это… Все ваши предложения уже были реализованы три десятилетия назад.

                                    Не взлетело (за исключением банков и тому подобных заведений почему-то).


                            1. tyomitch
                              22.08.2018 17:56

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

                              Как насчёт «распространять в виде переносимого байт-кода, компилировать в машинный код при первом запуске»? Как-то так делается на Андроиде, емнип.


                              1. DistortNeo
                                22.08.2018 17:59

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


                                1. tyomitch
                                  22.08.2018 18:12

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


                                  1. DistortNeo
                                    22.08.2018 18:28
                                    +1

                                    То есть пользователь запускает программу, офигевает от тормозов, потому что компиляция не в реальном времени, удаляет. Так, что ли?


                                    1. tyomitch
                                      22.08.2018 18:42

                                      Как-то так, да. Ничем не отличается от тормозов при автоапдейте: прогресс-бар и «please wait while we're improving your experience» без указания, сколько времени осталось ждать. К автоапдейтам же вроде все привыкли?


                                      1. DistortNeo
                                        22.08.2018 19:09

                                        Нет, я не привык.


                            1. novice2001
                              22.08.2018 18:11
                              -2

                              И сколько сотых долей процента производительности потеряет программа скомпилированная с другим таймингами?
                              И сколько займет jit-компиляция какого-нибудь «Хрома» или «Офиса»?

                              Ничего себе заявление

                              Смотрите комментарий выше.


                            1. 0xd34df00d
                              22.08.2018 18:13

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

                              О, мой сокурсник в ИСП РАН над чем-то таким работал. И, насколько я помню, с похожей мотивацией.


                              1. tyomitch
                                22.08.2018 18:17

                                Эта идея, прямо скажем, не нова.


                                1. 0xd34df00d
                                  22.08.2018 18:19

                                  Да это понятно, можно даже в P-код-виртуалки не лезть, а вспомнить про всякие дотнеты и JVM. Просто в том случае это было лет 7 назад, и это было именно LLVM именно для плюсов.


                                1. khim
                                  22.08.2018 19:18
                                  +1

                                  А разве P-код кто-то перекомпилировал? Его интерпретировали или JIT'или вроде.

                                  Перекомпиляция — это TIMI, 1988й год…


                      1. qw1
                        22.08.2018 16:51

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


                        1. novice2001
                          22.08.2018 17:51

                          Я тоже не совсем понимаю с чем именно вы спорите. Начиная с терминологии. Кроме того вы повторяетесь.
                          В Windows нет никакого «общего адресного пространства», адресное пространство (АП) каждого процесса отделено от АП любого другого процесса.
                          То, что вы можете загрузить недоверенный код внутри АП вашего же процесса неудивительно и естественно. Но получить доступ к АП других процессов (без помощи ОС) вы не можете.
                          И это

                          раздельные адресные пространства и прочее — это единственный вариант в мире где возможно исполнение недоверенного кода
                          вы просто не поняли. Естественно, что можно сделать систему, в которой АП будет общим. С этим никто и не спорит. Только по соображениям безопасности этого делать никто просто не будет. Поэтому, если из контекста для вас неочевидно, то я чуть-чуть переформулирую «раздельные адресные пространства и прочее — это единственный разумный вариант в мире где возможно исполнение недоверенного кода»


                          1. qw1
                            22.08.2018 21:11

                            это единственный разумный вариант
                            Не слишком ли категорично? Какие фундаментальные проблемы с безопасностью есть у Application Domains в .net?


                          1. qw1
                            22.08.2018 21:19

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


                    1. tyomitch
                      22.08.2018 15:36

                      А в чем смысл идеи VLIW, отдать шедулинг команд компилятору, когда и процессор неплохо справляется.

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


                    1. mayorovp
                      22.08.2018 19:30
                      +1

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


                      1. qw1
                        22.08.2018 21:12
                        +1

                        Логически — да, но для процессора это одно адресное пространство.

                        И тут нет тех накладных расходов на переключения, которые есть у адресных пространств процессов.


      1. babylon
        21.08.2018 08:46

        arteast, согласен. Вы вполне могли бы оформить свои комменты в отдельную статью. Тема то бездонная. Даже если не касаться работы с памятью.


    1. MaxVetrov
      21.08.2018 09:27
      -1

      Ошибаетесь.) Есть машинный код — он процессору ближе. =)


  1. EmmGold
    20.08.2018 18:57
    +5

    С каких пор язык си низкоуровневый? Тем более общепринято?


    1. robert_ayrapetyan
      20.08.2018 19:56
      +5

      А какой — общепризнано?


      1. EmmGold
        20.08.2018 21:03

        Мне попалась на глаза статья Дэвида Чизнэлла, исследователя Кембриджского университета, в которой он оспаривает общепринятое суждение о том, что С — язык низкого уровня, и его аргументы мне показались достаточно интересными.


      1. aamonster
        20.08.2018 22:22
        +3

        Ну, в те годы, когда я учил Си — его называли языком высокого уровня, изредка говоря "среднего" за некоторые низкоуровневые фишки типа адресной арифметики.


        1. quwy
          21.08.2018 02:43
          +1

          Адресная арифметика даже в некоторых Бейсиках есть. В статье совершенно справедливо упоминается PDP, для которого C был по сути макроассемблером. Для всех других архитектур C ничем не ближе к железу, чем Фортран или Паскаль.


          1. aamonster
            21.08.2018 21:18

            1. Так я и говорю — не называли его языком низкого уровня.
            2. Кроме PDP-11 было ещё много архитектур, на которые C ложился «достаточно прямо». В том же 8086 была сломана только адресация (сегмент-смещение, нигде больше не видел подобного ада), но она была сломана для всех языков. Зато были базовые регистры, в частности регистр BP, в который копировался SP — т.е. нормальная поддержка стек фреймов, что очень ценно для наивной реализации компилятора Си. И никакого предсказания переходов.
            Или hitachi sh3 — на котором я впервые увидел, как компилятор Си выдаёт предельно оптимизированный код. С delayed branches вместо предсказания переходов — гениальное решение.

            P.S. И ведь статья-то на итог вообще не об этом, и гораздо интереснее темы из заголовка…


    1. dipsy
      20.08.2018 20:26
      +3

      There is no official definition, but historically assembler/machine code was considered low-level and any language more abstracted was high-level. But C is one of the high-level languages which is closest to the machine level, which is why it is sometimes designated «mid-level», while scripting languages like Python have sometimes been designated «very high level». But these are all informal categories and somewhat subjective.
      ++ www.quora.com/Why-is-C-considered-a-low-level-language


    1. Azya
      20.08.2018 22:19
      +2

      Вас еще кто-то минусует. Тоже удивлен существованию дискуссии о высокоуровневости Си.


      1. Hardcoin
        21.08.2018 12:54

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


        1. Azya
          21.08.2018 13:17

          По всей видимости, те, кто называет Си низкоуровневым подразумевают некоторый неформальный контекст.


  1. marsianin
    20.08.2018 19:17

    Всё-таки ARM SVE — это *Scalable* Vector Extension, а не Scalar.


  1. DustCn
    20.08.2018 20:18
    +5

    Чего только на волне хайпа с этими практически умозрительными багами не напишут. И С уже стал резко высокоуровневым, и замедление от патчей отбросило перформанс на годы назад, аяй…
    Не можете понять как компилируется ваш С-шный код? Включите трансляцию в ассемблер и читайте. Придется правда в C ABI разобраться, но то такое.
    Не нравится что компилятор слишком умный? Поставьте -О0 и вперед — оптимизируйте руками.

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

    >>Эта информация в C гораздо более ограниченна, чем в таком языке, как Fortran, что является основной причиной того, что C не сумел вытеснить его из сферы высокопроизводительных вычислений.
    Фига с два. Основной причиной является то, что на фортране дочерта и больше Legacy кода, который тянется с мохнатых 80-х или 70-х, в том что в тех же годах математики и физики очень привыкли фигачить все на фортране. Практически до 2000-х, когда видимо начинать новый проект на фортране стало уж совсем моветон. Реализация С-шных и С++ вещей на фортране часто вызывает улыбки. Например чтобы преобразовать 2-д в 1-д массив без вызова решейпа (затратной операции, которая реально перекладывает данные) — фиганем его через функцию, в которой массив определим «как придет» — потеряем дескриптор, но зато можем заюзать его как угодно. Или реализация полиморфных объектов через интерфейсы…
    Короче не от хорошей жизни там извращаются.


    1. picul
      20.08.2018 21:27
      +1

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

      P. S. Про Фортран — полностью с Вами согласен.


    1. tyomitch
      21.08.2018 11:36

      Не можете понять как компилируется ваш С-шный код? Включите трансляцию в ассемблер и читайте. Придется правда в C ABI разобраться, но то такое.

      А если не можем понять, как (и сколько времени) выполняется наш ассемблерный код, потому что внутри процессора он транслируется в нечто недокументированное, тогда что читать? :-Р


      1. DustCn
        21.08.2018 13:52
        -3

        Документацию на процессор. В конце мануала вы найдете таблицу со всеми коммандами, у которых указан Latency и Throughput.


        1. khim
          21.08.2018 14:36
          +1

          Дык статья ровно о том, что последний процессор, для которого был такой мануал — это 80486. Появился в 1989м, не выпускается с 2007го.

          А если вы про эту — так она не из мануала, а из экспериментов и, кроме того, в ней масса ньюансов не описана.


          1. DustCn
            21.08.2018 17:07

            Всегда значит был, а тут вдруг не стало.
            Я не припомню процессора для которого не было таблицы с латентностями-трупутами, а занимаюсь я оптимизацией ПО уже более 10 лет.
            Вот вам по скайлейку несуществующая табличка от производителя:
            software.intel.com/sites/default/files/managed/ad/dc/Intel-Xeon-Scalable-Processor-throughput-latency.pdf


            1. khim
              21.08.2018 17:22

              В вашей табличке даже не написано на каких блоках какие команды можно исполнять впараллель. А это — очевидно необходимое (но не достаточное!) требование для ответа на вопрос «как (и сколько времени) выполняется наш ассемблерный код»!

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


              1. novice2001
                22.08.2018 13:50

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


            1. dimm_ddr
              22.08.2018 13:38
              +1

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


      1. dimm_ddr
        21.08.2018 15:21

        Тогда нужно устраиваться в компанию разработчик процессора и читать внутреннюю документацию. Или сломать их систему и достать доки нелегально. Или реверснуть логику процессора. Какой вариант вам больше нравится? =)


  1. Boroda1
    20.08.2018 20:19
    +3

    Могу судить по сфере встроенного ПО для микроконтроллеров разных архитектур.

    Самое низкоуровневое, что доступно программисту — это машинные коды. Машинные коды можно хоть по бумажной таблице перевести в ассемблер (и обратно) даже вручную.
    При этом компиляторы C выдают соответствующий ассемблерный код такого качества, что даже если бы я переделывал эти задачи вручную на ассемблере, доля совпадений была бы порядка 90%, если не более.

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


    1. Ardanay
      20.08.2018 20:32

      Да как бы, уже очень давно сами по себе х86 инструкции для процов — это такой же байткод как и Java и .Net


      1. Boroda1
        20.08.2018 20:38

        Я намеренно указал, что архитектуры не ограничиваются x86


        1. Ardanay
          20.08.2018 20:45

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


          1. slonopotamus
            20.08.2018 23:28
            +2

            Нельзя, у машины Тьюринга бесконечная лента.


      1. Azya
        20.08.2018 22:33

        Байт-код Java это язык низкого уровня, а ЯП Java это язык высокого уровня.


    1. Azya
      20.08.2018 22:30

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


    1. Siemargl
      20.08.2018 22:40

      Есть еще микрокод процессора.


      1. Azya
        20.08.2018 22:43

        Он же не доступен для программиста, поэтому, думаю, рассматривать его в качестве ЯП не очень корректно.


        1. Siemargl
          20.08.2018 23:15

          Думаешь, на нем офисные работники пишут, а не программисты? =)

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


          1. Azya
            20.08.2018 23:27

            На нем пишут разработчики конкретного ЦПУ в процессе его создания, это совершенно не то же самое. Т.е. микрокод является его неотъемлемой частью и рассматривать его как язык способный программировать ЦПУ в целом — это уже какой-то уроборос получается


            1. Siemargl
              20.08.2018 23:40

              Нет уробороса. Ниже уже электросхемотехника, так что все — .


              1. Azya
                20.08.2018 23:47

                Микрокод это неотъемлемая часть конкретного ЦПУ, меняя микрокод вы меняете ЦПУ а не программируете его.


                1. MikailBag
                  21.08.2018 00:38

                  Микрокод не является неотъемлемой частью процессора, потому что относительно легко заменяется. (Без физических манипуляций).


                  WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
                  
                  amd64-microcode/bionic-updates,now 3.20180524.1~ubuntu0.18.04.2 amd64 [installed,automatic]
                  intel-microcode/bionic-updates,now 3.20180425.1~ubuntu0.18.04.2 amd64 [installed,automatic]
                  microcode.ctl/bionic 1.18~0+nmu2 amd64


                  1. Azya
                    21.08.2018 00:51

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


                    1. Azya
                      21.08.2018 01:11

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


          1. MacIn
            21.08.2018 01:49

            Это очень старая идея. Когда-то мой отец занимался тематикой трансляции Фортрана в микрокод, еще в советское время.

            Один чешский деятель, сейчас не вспомню университета и регалий, реализовал это году в 2008 — трансляция С в микрокод (не помню, какой именно, не суть).

            Моя дипломная работа посвящена в чем-то аналогичной идее — доступу напрямую к микрокоду.


          1. 5oclock
            21.08.2018 09:04
            +1

            Вы имеете ввиду VLIW?


            https://ru.m.wikipedia.org/wiki/VLIW


        1. saboteur_kiev
          21.08.2018 16:06

          Почему недоступен?
          Зависит от процессора и от программиста.


          1. Azya
            21.08.2018 16:45

            Я только могу еще раз повторить то же, что отвечал чуть выше Siemargl'у.


  1. Boroda1
    20.08.2018 20:54

    Раз уж там упоминался SPARC.
    Весной к нам в контору приезжали представители МЦСТ и рекламировали помимо своих VLIW Эльбрусов свой же компилятор С.
    Так вот судя по их словам, обогнать себя самого на VLIW-ассемблере либо нельзя, либо на считанные проценты, относительно того же грамотно написанного С-кода в связке с оптимизирующим компилятором.
    Так что будь то ARM, SPARC или что-либо ещё — суть не меняется.
    Ниже машинных кодов ни в одной архитектуре не опустишься. И С даёт результат чрезвычайно близкий к вручную написанным машинным кодам.


    1. old_bear
      20.08.2018 23:10

      Ниже машинных кодов ни в одной архитектуре не опустишься.

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

      Но это не точно. (с) По крайней мере в тех кусках, с которыми я ковыряюсь на предмет оптимизации на asm-е.


      1. Boroda1
        20.08.2018 23:16

        Именно поэтому я и написал про 90%. В тех редких случаях, когда компилятор всё-таки так и не догадался, как именно можно было срезать углы. И это вопрос качества С-кода и качества компилятора.


        1. Duduka
          21.08.2018 08:00

          Существенно менее 90%.
          Ложь скрывается в том, что это только для программ написанных квалифицированными программистами, знающими архитектуру для которой пишут, как минимум на уровне ассемблера. Код скопированный со «стековерфлоу» без «творческой» обработки на 100%, или из-за не знания алгоритмов, или неадекватности алгоритма под конкретную архитектуру (где важны учет операций пересылки, ветвления, а не количества арифметики или количества сравнений, и прочая, и прочая...).
          Ложь заключается в том, что си низкоуровневый только в том, что он не предоставляет инструментария, доступного в языках высокого уровня: он не дает никаких гарантий; код на ассемблере или блисе, чуть более низкоуровневый, только потому, что там работа с типом еще менее ограниченна (есть, но никто, ничего гарантировать не может!).
          Языки низкого уровня — это языки которые выбирают ССЗБ, разменивая простоту и ограничения абстрактных машин ( DSL[language] -языков ) на максимальную доступность функциональности архитектуры на текущих ЦПУ, минимизируя ответственность за качество кода, выразительность, обозримость, управляемость коллективной разработки (всем все доступно, можно трюкачить, зная «что внутри»; как результат было больше ограничений встроено в си++, java… языках высокого уровня и ООП, с их инкапсуляцией… правда с тех пор мы потеряли Пакеты Прикладных Программ(этакий сорт библиотек, которые мы тоже потеряли)… ССЗБ).
          Язык — это всегда! система ограничений [как и языки человеческого общения], я говорю не только о синтаксисе/семантике (да, и они входят в эту систему), но о том, что эта система строится для удобства получения конкретного! результата, через высокоуровневую систему типов (функциональность, в си реализовано только покрытие модальных целых, реализованных в ЦПУ, даже с float/double принято решение в пользу усеченных, в сравнении с фортрановскими или ди, SIMD — чисто архитектурные, а матрицы реализованы только в языках-матпакетах, работа с временем, цветом, видео… скоро (видимо) будет только через #nodejs, на amazon cloud, а пользователю будет отсылаться только картинка со звуком, получая управленческие импульсы прямо из мозга человека-обезьянистого [сарказм] ). Си — это язык unix, как было задумано, использование его иначе чревато овердрайвом, OS обязаны (теперь!) предоставлять всю (открытую) функциональность операционки из конца 60х, ее безответственность по отношению к пользователям, требуя высокой квалификации и обоймы программ накладывающих ограничения на это пиршество свобод…
          Конечно, идеалом не является программа с единственной кнопкой «сделай мне хорошо», но язык не определяется свободами, язык — это средство общения, как идеал, си не подходит более чем полностью, он не может даль ничего более (в сравнении с ассемблерами, тем более с макро-… или тем же occam-2, а тем более с ada или c++). Единственное, что дает си — это качественные (на текущий момент) оптимизирующие компиляторы, и легаси, накопившееся с доисторических времен, когда компьютеры были большими.


    1. gorodnev
      21.08.2018 01:56

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


      А самое интересное еще впереди — интернет-гиганты пытаются применять машинное обучение к построению hardware. То же самое происходит и по направлению к компиляторам. К сожалению, быстрое гугление пока не дало результатов. На мой взгляд, стоит ждать скорого сокращения разрыва между лучшими оптимизаторами в лице человека и оптимизаторами компилятора.


  1. bugdesigner
    20.08.2018 22:59
    +4

    В своё время нам так разъясняли, чем отличаются языки высокогоуровня от низкого — наличием абстракции. По скольку типы данных и операции языка С абстрагированы от архитектуры — он является языком высокого уровня по определению. Например, такие вещи, как операции с плавающей точкой, являются частью стандарта языка С, однако некоторые архитектуры имеют только целочисленные АЛУ, но благодаря компилятору, программисту не надо об этом заботиться. Разработчики, использующие языки с бОльшим уровнем абстракции, как GO, например, считают С языком низкого уровня, но это не так.


    1. vsb
      21.08.2018 02:04

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


      1. bugdesigner
        21.08.2018 08:56

        Не нужно путать мнемокод ассемблера и машинный код. Машинный код это двоичные данные, которые непсредственно исполняет процессор, и ничего там уже не переименовывается и не изменяется. Есть ещё байт-код, например Java, но он исполняется не непосредственно процессором, а run-time интерпретатором — специальной программой, которая превращает байты этого кода в вызовы специальных подпрграмм — он так же не является машинным кодом!
        Если Вы на assembler напишете программу, то каждая инструкция будет однозначно преобразована в машинный код, и если дезассемблировать полученный дамп данных, вы получите практически тот же исходный код, единственное — это символические имена меток и адресов памяти будут в виде цифр. Поэтому assembler и является языком низкого уровня. Вам нужно знать все спецификации процессора, для которого пишетсчя программа — никакой абстракции, bare metal!
        Чтобы пояснить наглядно разницу низкоуровневого ассемблера и высокоуровнего С, приведу маленький пример из жизни 8-битных микропроцессоров AVR. В серии megaAVR есть блок умножения и соответствующая инструкция mul, а в tinyAVR такого блока и инструкции — нет. То есть, написанная программа на ассемблере, для megaAVR с использованием инструкций mul не может быть скомпиллирована для tinyAVR. Все операции умножения прийдется заменить подпрограммой умножения, которая использует, допустим, сложение в цикле. Это говорит о том, что программист должен знать сиецификацию целевого процессора, набор команд, регистров итд. В С можно просто написать a=b*c; и это без переделок можно скомпиллировать для любого процессора, даже не задумываясь, может ли его АЛУ перемножать числа или нет. Компилятор языка С сам заменит умножение на нужрую инструкцию или подпрограмму, в зависимости от используемого целевого процессора.


        1. warlock13
          21.08.2018 10:50
          +2

          Машинный код это двоичные данные, которые непсредственно исполняет процессор

          Для x86 это и близко не так. Собственно, про это и статья.


        1. khim
          21.08.2018 11:41

          Машинный код это двоичные данные, которые непсредственно исполняет процессор, и ничего там уже не переименовывается и не изменяется.
          На календарь смотрели? Последний x86 процессор, где ничего не переименовывалось и не изменялось (хотя кеши были уже и там) — это Pentium! Вышедший четверть века назад!

          И для ARM-процессоров — это тоже уже не так (про Denver почитайте, что ли).

          Да, в embedded это ещё временами так, ну так в статье не про это речь…


        1. RomanArzumanyan
          21.08.2018 15:22

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


  1. BalinTomsk
    20.08.2018 22:59

    ----а делали быстрые процессоры с интерфейсом PDP-11.

    Мне кажется автор имеет весьма смутное понимание о машинах PDP-11.

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

    Интел тут со 180 коммандами в паралель выглядит бледно.

    Слово эмуляция, мне кажется, неуместно.


    1. DareDen
      22.08.2018 09:28

      Да, точно подмечено. У PDP-11 не было ни суперскалярности, ни предсказаний, ничего, что привело к появлению спекулятивных уязвимостей. Ортогональная система команд, один из лучших ассемблеров, который я видел. До сих пор помню часть memcpy :)
      mov @(r0)+, @(r1)+


  1. beeruser
    20.08.2018 23:00
    -1

    VLIW Эльбрус основан на архитектуре E2K, а не SPARC.

    И С даёт результат чрезвычайно близкий к вручную написанным машинным кодам.

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


    1. Boroda1
      20.08.2018 23:12
      +1

      Вы не читали мой комментарий.
      Современные компиляторы выдают машинный код практически идентичный по качеству тому машинному коду, который я могу написать вручную, зная все особенности данной архитектуры.
      Я нигде не говорил, что С-код впрямую транслируется в машинные коды. Я говорил о том, что компилятор выдаёт машинный код, который мне очень сложно обогнать, решая ту же задачу на ассемблере (или в машинных кодах). Мой машинный код получится на 90% идентичный тому, что сгенерировал компилятор.

      А Эльбрус — это ещё и R-серия, которая чистый SPARC.


      1. Dima_Sharihin
        21.08.2018 08:15

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

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


        Что же до "90% идентичный", то компиль обладает замечательной возможностью перемешивания инструкций на конвейере, что позволяет выполнять код быстрее его прямолинейного составления и в итоге он должен стать даже быстрее того, что пишет человек


  1. Azya
    20.08.2018 23:16

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


  1. DistortNeo
    20.08.2018 23:57
    +1

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

    С тех пор было создано огромное множество языков, и все они — одинаково высокоуровные согласно этой классификации. C, C++, C#, JavaScript, Python, да даже LLVM — всё это высокоуровневые языки. В этом случае подобная классификация не имеет никакого смысла.

    То есть имеет смысл в относительной классификации, а не в абсолютной, или в нескольких уровнях градации высокоуровневых языков:
    — абстрагирование от работы с регистрами и машинным представлением данных (C­);
    — наличие структур данных: объектов, функций высших порядков (C++)
    — абстрагирование от прямой работы с памятью (.NET, JVM);
    — абстрагирование от операционной системы (JavaScript).


    1. kovserg
      21.08.2018 00:18

      Если javascript решает задачу абстрагирование от операционной системы — то он её ниразу не решил.


      1. Goodkat
        21.08.2018 12:00
        +1

        Как минимум один раз решил — моё приложение одинаково работает на айфоне, андроиде, в файрфоксе на убунту, в сафари на маке и хроме на винде.
        Так что ваше заявление ложно.


        1. iCpu
          21.08.2018 12:30

          Да-да, моя Qt программа тоже без проблем собирается на винде и убунту, но есть нюансы

          Для точекнету и жабомашины ОС — виртуальная машина. Всё было круто и однородно чуть ли не до омерзения, когда мобильные апплеты можно было запускать на пеке. А потом появились OpenJDK и Mono — и ряд программ перестал нормально работать в одной из реализаций. Не говоря уж о том, что не только язык, но и байткод обоих терял обратную совместимость в ряде аспектов.

          Для бинариков тоже есть разные приблуды. Вроде WINE, которые симулируют x86 и x64 окружение Windows, сохраняя весьма приличную производительность. Или обратную фишку для POSIX от мелкософта. Всё вроде работает, но нет совместимости баг-в-баг.

          Для жабоскрипта ОС — это среда выполнения, обычно, браузер. Внезапно в хроме, огнелисе, сафари и эдже есть куча собственных костылей, которые нужно учитывать, а ведь есть ещё node и другие ребята. Дабы далеко не ходить, как давно в них во всех появилось общее имя для корневого элемента? У вас одинаково — или у меня вся вёрстка рассыпется на левом браузере с кастомной темой? Да ладно, у меня в Вивальди или, ть, Амиго точно всё работать будет?


          1. khim
            21.08.2018 13:43

            Да ладно, у меня в Вивальди или, ть, Амиго точно всё работать будет?
            А что это за операционные системы такие? Я таких не знаю.

            Зависимости от операционной системы таки больше нет. А от браузера — да, таки есть. Что вас удивляет?


            1. iCpu
              21.08.2018 13:52

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


              1. khim
                21.08.2018 14:39

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


          1. DistortNeo
            21.08.2018 18:16

            Для точекнету и жабомашины ОС — виртуальная машина.

            Но и там, и там есть возможность взаимодействия с нативным кодом, возможность вывоза функций системы и взаимодействие с её сущностями.


            А в JavaScript и этого нет, взаимодействие идёт только с браузером.


            Для жабоскрипта ОС — это среда выполнения, обычно, браузер. Внезапно в хроме, огнелисе, сафари и эдже есть куча собственных костылей, которые нужно учитывать, а ведь есть ещё node и другие ребята.

            Если бы существовала серебряная пуля, было бы легче, правда?


    1. Azya
      21.08.2018 00:43

      С тех пор было создано огромное множество языков

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


    1. KvanTTT
      21.08.2018 01:46

      .NET и JVM тоже абстрагированы от операционных систем, во всяком случае десктопных.


  1. kovserg
    21.08.2018 00:09
    +2

    1. Вообще не понятно причет тут язык в частности C. Это проблемы другого плана.
      Современные программы тормозят совсем по другим причинам. Компилятор тут вообще не причем. Главная причина экономическая. Надо получать прибыль быстро, поэтому разработчики имеют топовое железо, что бы быстрее выкатить хоть как-то рабочую версию. У них работает приемлемо и ладно. То что у пользователя тормозит — это проблемы негров. Пусть покупают новое железо. Взгляните на java — академически правильный язык, быстрый, хорошо структурированный автоматически следит за память… всё шикарно. Но программы на java приобрели славу тормозного, жрущего ресурсы по. Почему да потому что там тысячи абстракций, куча библиотек обёрнутых в другие библиотеки да еще всё это разных версий.
      Если старый word 2003 запустить на новой машине. Всё работает просто мгновенно по стравнению с word 2016, хотя решают они одинаковые задачи набор и печать текста и компиляторы стали лучше и делают массу трансформаций, и операционка новее и ядер больше. Но когда курсором мигают через javascript это всё коту подхвост
    2. Паралельный код писать очень сложно и поддерживать дорого. Особенно на языках к жтому не приспособленных. А использовать всю вычислительную мощность конкретной системы иногда вообще невозможно. Плюс массивно паралельных систем сейчас очень много видов. Поэтому приходится использовать абстракции openmpi,opencl,openal,… и готовые готовые библиотеки IPP, gpu-accelerated-libraries… Более того некоторые алгоритмы не имеют общих рецептов распараллеливания.
    3. В модель акторов не панацея она создаёт больше проблем чем решает.
    4. Высокоуровневый язык это sql вы ему пишете что хотите, а он уже сам строит план выполнения, выполняет и возвращает вам результаты.
    5. Язык C полный по тюрингу и на нём теоритически можно написать что угодно. C компилятор есть почти под любую платформу. Если нет то его просто написать, т.к. сам язык простой.
    6. Сам язык прост в изучении. Главное отличие C от C++. C необходим для написания маленьких утилит которые потом объеденяются в рабочую систему языками склейки (bash,python,lua,java...). C в отличии от C++ имеет вменяемый бинарный интерфейс. С другой стороны C++ претендует на то чтобы писать огромные «титаники» в которых весь функционал пихают в одну программу.
    7. Постоянное развитие C++ ведёт его в сторону усложнения, как для изучения так и для написания компиляторов. И к сужению количества платформ под которые можно компилировать. При этом не решая практических задач. (например php из коробки умеет общаться с базами данных, работать с архивами и т.п.)
    8. Более того не всегда ясно какое наречие языка C++ надо использовать, что бы не огрести проблем.
    9. Для решения задач распараллеливания есть экспериментальные языки типа halide-lang.org которые пытаются дать инструмент для анализа и поиска компромисса локальности, параллельности и порядка вычислений.

    ps: Главная нерешенная задача в программировании это борьба со сложностью.


    1. stalkerg
      21.08.2018 04:48

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

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


    1. iCpu
      21.08.2018 05:08

      1.1. Я эту старую песню о главном слышу с 2003 года. «Процессоры стали сильно мощные, их нужно замедлять искусственно, чтобы покупали новые.» Это очень красивые слова, которые нужно доказать метриками. А ещё лучше, разбором топологии. А с этим будет очень непросто. А пока это не сделано, к подобным фразам можно относиться так же, как к вою о вреде ГМО.
      1.2. Очень красивый пример с вордом. Но давайте ближе к нашим тушкам. Давайте сравним студию 2003 и 2017. Начните с тормозной новой, а потом перейдите на быстролётную старую. Только попытайтесь не блевать в процессе.
      2. Да и нет. MPI и прочие — они не про параллельность выполнения операций. Они о командной работе. Про параллельность — OpenMP. ИМХО, там достаточный набор команд для нормального распараллеливания задачи. Если вам нужно больше — вы пытаетесь решать несколько задач одновременно.

      Остальные пункты вообще непонятно о чём. Вы не расчёсывайте плюсы — они и чесаться у вас не будут.

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

      int __declspec(allocate(L3Cache)) bigRoutineCounter; 

      Но нам, почему-то, не доверяют делать вот такие советы. Вот это, на самом деле, печально и обидно.


      1. Wedmer
        21.08.2018 06:50

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


        1. iCpu
          21.08.2018 07:30

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

          К тому же, что это за аргументация? Это ж бред! Как минимум, разработчики компиляторов, игр и высоконагруженных систем кровно заинтересованы в каждой доле процента производительности. Если размещение простых ничего не обязывающих прагм работы с кешем позволит повысить производительность на 1% — это уже серьёзно. Если дать возможность программисту задавать вероятности выполнения условий — и передавать их прямо в процессор, можно ожидать огромный скачок производительности всего кода, особенно, JIT.

          Пускай это будет расширение Intel или AMD. Пусть это будут «рекомендации» типа inline. Пускай 99% кладут на оптимизацию. У оставшегося процента программы будут значительно быстрее, а зарплаты — выше. И это прекрасно, даже если меня в этом проценте нет и не будет.


      1. orcy
        21.08.2018 08:34

        > Давайте сравним студию 2003 и 2017

        Кстати предпочитаю использовать Visual Studio 2005, а не 2017 (там где требуется старый уровень С++). Заметно быстрее работает и все что надо в основном есть (отладка, редактирование, навигация). Там где можно использовать новый С++, там понятно лучше новая студия (ибо старая перестает работать как IDE), но каких-то особых плюсов я не вижу (кроме «Rename» — может чего не нашел?), а минусы в виде пониженной производительности и повышенного потребления ресурсов я замечаю.


        1. iCpu
          21.08.2018 09:19

          Давайте немного отвлечёмся от плюсов, так как MS явно не горит пламенной любовью к крестовикам и поддерживает их сравнительно бедно. Сравните редактор C#. Сравните подсветку и автодополнение, построение схем кода и тп. Они разительно отличаются. Тот же IntelliSense превратился из «о, боже мой, как это отключить» во вполне себе полезный инструмент, которого вполне хватает. И так во всём.

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


      1. khim
        21.08.2018 11:48
        +2

        1.2. Очень красивый пример с вордом. Но давайте ближе к нашим тушкам. Давайте сравним студию 2003 и 2017. Начните с тормозной новой, а потом перейдите на быстролётную старую. Только попытайтесь не блевать в процессе.
        Блевать может захотеться от ограничений старых версий компилятора, но вот UI у старой версии — таки приятнее и удобнее.


      1. serbod
        21.08.2018 14:58

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

        Я пишу софт на Паскале для большого зоопарка систем от первых Селеронов и Атлонов (даже без SSE) до современных 18-ядерных. Который должен работать годами без остановки. И так получается, что если отвязать логику от гуя и от ввода-вывода (через очереди команд), то многое можно сделать асинхронным и легко распараллелить. И тогда проблем с быстродействием процессора практически не возникает, все начинает упираться в диск, сеть и память. И разница между системами 15-летней давности и современными оказывается не столь велика — везде есть какие-то узкие места, то в рейд-контроллере через неделю работы деградирует кеширование запись, то запись в SSD деградирует, то вдруг ОС решит дисками пошуршать по своим делам, то память фрагментируется до неприличия, не может найти непрерывный кусок в десяток мегабайт при свободном десятке гигабайт… А у процессора на самом деле моща гигантская, сотни миллионов операций в секунду. И эта моща может легко уйти в свисток в каком-нибудь банальном цикле поиска или сравнения строк, выполняемого для каждой ячейки таблицы. Для которых есть специальные команды процессора, но компилятор и библиотеки языка программирования решили, что для уникода это не годится.


        1. iCpu
          21.08.2018 15:54

          Пардон пур мон франсе, а разве паскаль ещё жив? Последний стандарт был в прошлом веке. Быть может, вы о делфи? Но и с ним всё не так хорошо. Насколько я знаю, он последние лет 6 мучительно агонизирует. Тем более, ни первый, ни второй никогда не претендовали на поразительную скорость и близость к железу. Так чего ждать от старых компиляторов?

          И, вы серьёзно? То, что вы описываете — отсутствие гигиены, причём, принудительное. Та же дефрагментация по умолчанию выполняется по расписанию еженедельно. Если перестать убирать пыль, любой ПК вышедший после 1996 года рано или поздно вызовет пожар. То же в ОС. У меня на моих задачах даже между Core2Duo 2008 года и Athlon2x4 2011 года разница ощущается весомая. Если вы i9 увайдохиваете в днище… Остаётся только снять шляпу перед вашим мастерством!


          1. serbod
            21.08.2018 16:54

            И, вы серьёзно? То, что вы описываете — отсутствие гигиены, причём, принудительное. Та же дефрагментация по умолчанию выполняется по расписанию еженедельно.

            Как дефрагментировать память в Windows без перезапуска приложения? =)

            На процессоре i3 обычно нагрузка 1-3%, на старых 10-20%. Там просто данные из разных портов обрабатываются и отправляются в сеть + кладутся на диск в виде логов и БД. Но данных много, в сутки до 2 Гб логов.


            1. 0xd34df00d
              21.08.2018 17:02

              Как дефрагментировать память в Windows без перезапуска приложения? =)

              Смотря как приложение написано и как оно работает с памятью. Любой compactifying GC именно что дефрагментацией и занимается как побочный эффект.

              Ну и зачем заниматься дефрагментацией на 64 битах, скажем, я не очень понимаю. Да, есть TLB, есть page cache, но это уже всё эффекты следующего порядка малости.

              Но данных много, в сутки до 2 Гб логов.

              Ух ты.


              1. serbod
                21.08.2018 17:12

                Ну и зачем заниматься дефрагментацией на 64 битах, скажем, я не очень понимаю. Да, есть TLB, есть page cache, но это уже всё эффекты следующего порядка малости.

                Это в теории на 64 битах все должно быть шоколадно, а на деле оно вафельно. Вот у чувака похожие проблемы: habr.com/post/420579


                1. 0xd34df00d
                  21.08.2018 18:48

                  Это, как я понял, какие-то проблемы в реализации ОС (которые, тем более, как он пишет, позже были пофикшены в самой ОС), а не непосредственные свойства режима адресации.


              1. novice2001
                22.08.2018 15:57

                Ну и зачем заниматься дефрагментацией на 64 битах, скажем, я не очень понимаю

                Каким образом разрядность ОС влияет на проблему фрагментации памяти?


                1. DistortNeo
                  22.08.2018 16:10
                  +1

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


                  1. novice2001
                    22.08.2018 16:25

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


                    1. DistortNeo
                      22.08.2018 17:57
                      +2

                      А почему фрагментация физической памяти — это проблема? И зачем её решать? Или вы пользуетесь операционной системой без виртуализации доступа к памяти?


                    1. 0xd34df00d
                      22.08.2018 18:15

                      Достаточно не очень удачно выделить порядка 2048 мегабайт, чтобы больше ничего выделить не удалось. Ну, на 32 битах без PAE и со стандартными (насколько я помню, для Windows так) двумя гигами виртуальной памяти на юзерспейс.

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


        1. Dima_Sharihin
          21.08.2018 16:07

          то память фрагментируется до неприличия

          При частом выделении/освобождении памяти обычно применяют пулы объектов вместо кучи, не?


          в каком-нибудь банальном цикле поиска

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


          для уникода это не годится.

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


          Который должен работать годами без остановки

          ОС решит дисками пошуршать по своим делам

          У меня возникает ощущение, что где-то взяли не подходящий инструмент для решения задачи.


          1. serbod
            21.08.2018 16:42

            При частом выделении/освобождении памяти обычно применяют пулы объектов вместо кучи, не?

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

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

            В моем случае это не критично, там больше проблемы с ОСью.

            У меня возникает ощущение, что где-то взяли не подходящий инструмент для решения задачи.

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


      1. RomanArzumanyan
        21.08.2018 15:29

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


    1. picul
      21.08.2018 13:03

      openal
      — это библиотека для работы со звуком.
      java — академически правильный язык
      Ой, ну скажите еще, что С — не низкоуровневый язык))) Чем java «академически правильная»?

      А вообще все Ваши пункты никак не опровергают и не подтверждают сказанного автором.


      1. saboteur_kiev
        21.08.2018 16:08

        Си — высокоуровневый язык программирования.
        Изначально был высокоуровневым и им и остался.

        То, что за годы компиляторы для С/С++ сделали качественный скачок, не означает, что язык как-то изменился.


        1. kovserg
          21.08.2018 16:24

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


          1. saboteur_kiev
            21.08.2018 18:50

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

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


        1. picul
          21.08.2018 16:43

          Не спорю, это лишь отсылка к названию статьи.


      1. kovserg
        21.08.2018 16:22

        Извините ночью писал: Не openal а openacc
        Я вообще не понял что хотел сказать автор. Но категорически с ним не согласен.


    1. 0xd34df00d
      21.08.2018 16:40

      В модель акторов не панацея она создаёт больше проблем чем решает.

      А что там с акторами? Я топлю не совсем за них (меня больше STM и подобное воодушевляет), но всё равно интересно.

      Постоянное развитие C++ ведёт его в сторону усложнения, как для изучения так и для написания компиляторов.

      На современных плюсах легче писать, чем на плюсах 10-летней давности. И выучить современное подмножество, наверное, проще.

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

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

      Пхп-код уже можно компилировать под attiny?


      1. DistortNeo
        21.08.2018 18:31

        На современных плюсах легче писать, чем на плюсах 10-летней давности. И выучить современное подмножество, наверное, проще.

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


        1. 0xd34df00d
          21.08.2018 18:49

          Это просто многое говорит о таких собеседованиях.


        1. picul
          21.08.2018 19:27

          И правда, давайте не будем использовать языки, по которым можно придумать сложные собеседования. Всем бегом учить Go!


          1. 0xd34df00d
            21.08.2018 19:51
            +1

            Я тут в последнее время начал собирать файлик со списком вопросов по С++, и там есть такие, например.


            Соберётся ли такой код?


            struct Foo
            {
                Foo() = delete;
            };
            
            Foo foo {};

            В чём отличие этих функций?


            template<typename T>
            T mkFoo1() { return {}; }
            
            template<typename T>
            T mkFoo2() { return T {}; }

            Что выведет эта программа?


            #include <iostream>
            
            struct Foo1
            {
                int a;
            };
            
            struct Foo2
            {
                int a;
                Foo2() = default;
            };
            
            struct Foo3
            {
                int a;
            
                Foo3();
            };
            
            Foo3::Foo3() = default;
            
            int main()
            {
                Foo1 foo11, foo12 {};
                Foo2 foo21, foo22 {};
                Foo3 foo31, foo32 {};
            
                std::cout << foo11.a << std::endl;
                std::cout << foo12.a << std::endl;
                std::cout << foo21.a << std::endl;
                std::cout << foo22.a << std::endl;
                std::cout << foo31.a << std::endl;
                std::cout << foo32.a << std::endl;
            }


            1. picul
              21.08.2018 20:15

              1. Должен.
              2. Вторая сразу станет искать конструктор, а первая поищет оператор приведения.
              3. Думаю, не определено.

              Уверен, что ошибся минимум в двух пунктах из трех. Так что, учим Go?


              1. 0xd34df00d
                21.08.2018 20:26
                +1

                2. Оператор приведения из чего к чему?
                3. Да. На самом деле это вопрос о том, какие именно строчки надо закомментировать, чтобы стало определено (спасибо, подкорректировал у себя в файлике формулировку вопроса).

                А учить бы я стал какой-нибудь ATS. Или Rust, в конце концов.


                1. picul
                  21.08.2018 21:08

                  2. Initializer list'а к T.
                  3. Не знаю, у меня три варианта и все «равноправные»))

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


                  1. 0xd34df00d
                    21.08.2018 21:14
                    +1

                    Не, ну как, это следствие зоопарка из способов инициализации и количества различных видов типов данных (user-provided ctor, user-declared ctor, вот это всё). И это иногда выстреливает. По крайней мере, на code review на некоторые частные случаи этого всего мне указывать приходилось.


                    От Rust'а у меня тоже глаза немножко вытекают, но лично меня потихонечку начинает утомлять количество костылей и легаси в плюсах. Я при этом ни в коем случае не агитирую за простые и дубовые языки (это если возвращаться к вашему исходному тезису), но мне бы всё же хотелось, чтобы сложность языка не особо сильно отличалась от, так сказать, intrinsic complexity выражаемых им абстракций. Понимать интуиционистскую логику и теорию типов, если хочешь тыкать в proof assistant'ы, полезно. Запоминать стопицот способов инициализации и классов типов для написания числодробилок или, чего хуже, клиентов к СУБД, уныловато.


                    А вообще нет в жизни счастья, всё тлен. Пишу биндинги к libclang на хаскеле, так без адекватных зависимых типов очень больно выражать вещи вроде «значение литерала имеет смысл только для курсора, имеющего тип IntegerLiteral или StringLiteral или..., при этом значение литерала имеет соответствующий тип». На синглтонах это всё уродливо ппц. А на идрисе зато документации по FFI толком нет, да и библиотек тоже толком нет.


              1. khim
                21.08.2018 20:51

                Со вторым ошиблись. А это ведь как раз прекрасная иллюстрация обсуждаемого тезиса. В C++17 между этими двумя вариантами нет разницы, а в C++14 есть.

                Вот расширенный пример, показывающий разницу:

                $ ./clang++ -std=c++14 -fno-elide-constructors tst.cc -o tst && ./tst
                Foo() constructor called!
                Foo() constructor called!
                Foo(const Foo&) constructor called!
                $ ./clang++ -std=c++17 -fno-elide-constructors tst.cc -o tst && ./tst
                Foo() constructor called!
                Foo() constructor called!
                $ cat tst.cc
                #include <stdio.h>
                
                class TraceFoo {
                 public:
                  TraceFoo() { printf("Foo() constructor called!\n"); }
                  TraceFoo(const TraceFoo&) { printf("Foo(const Foo&) constructor called!\n"); }
                };
                
                template<typename T>
                T mkFoo1() { return {}; }
                
                template<typename T>
                T mkFoo2() { return T {}; }
                
                int main() {
                  mkFoo1<TraceFoo>();
                  mkFoo2<TraceFoo>();
                }
                


                1. 0xd34df00d
                  21.08.2018 21:03

                  С первым в С++20 тоже ошибочка уже будет, скорее всего. Такое поведение было решено запретить.


                  А со вторым, более того, разница не только в вызываемых конструкторах, но и вообще в поддержке noncopyable-типов. То есть, с noncopyable-типом первая функция протайпчекается, а вторая — нет. Даже без -fno-elide-constructors.


                  1. khim
                    21.08.2018 21:10

                    А со вторым, более того, разница не только в вызываемых конструкторах, но и вообще в поддержке noncopyable-типов. То есть, с noncopyable-типом первая функция протайпчекается, а вторая — нет. Даже без -fno-elide-constructors.
                    В C++17 обе прочекаются. В этом и был смысл изменения.


                    1. 0xd34df00d
                      21.08.2018 21:15

                      Да, конечно, я о C++ \leq 14, пардон. Прелести guaranteed RVO не в какой-то там производительности, а в более адекватной семантике кода. Можно теперь фабрики мьютексов без костылей писать.


            1. khim
              21.08.2018 20:18

              Последний вопрос — он вообще какой-то странный. Там же undefined behavior, так что она вообще всё, что угодно может вывести.


              1. 0xd34df00d
                21.08.2018 20:26

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


            1. DistortNeo
              21.08.2018 21:26

              1. Ответ зависит от версии компилятора. Берём компилятор, собираем. Может собраться, а может и нет.

              2. Ответ зависит от версии компилятора.

              3. Вообще дичь какая-то.


              1. 0xd34df00d
                21.08.2018 21:28

                1. Нет, пока что ответ от версии компилятора не зависит. Это aggregate initialization, поэтому соберётся во всех conforming-компиляторах для всех С++11, 14 и 17.


                2. Скорее от версии стандарта. В C++11 и 14 разница есть, в 17 разницы нет.


                3. Да, С++ такой.



                1. DistortNeo
                  21.08.2018 21:35

                  1. Нет, пока что ответ от версии компилятора не зависит. Это aggregate initialization, поэтому соберётся во всех conforming-компиляторах для всех С++11, 14 и 17.

                  Абсолютно нелогичное поведение, кстати. И хорошо, что в С++20 это пофиксят.


                  P.S. Не удивлюсь, если скоро в первой строчке файла придётся писать директиву типа #language c++20.


                  1. 0xd34df00d
                    21.08.2018 21:38

                    Да, согласен.


                    Ещё круче и WTF'нее, когда там параметры есть, типа


                    struct Foo
                    {
                        int i;
                        char c;
                    
                        Foo(int, char) = delete;
                    };
                    
                    Foo foo { 10, 'a' };


      1. kovserg
        21.08.2018 23:57

        Модель акторов внутри себя имеет очереди которые имеют свои особенности. Так вот когда они всплывают становиться очень не смешно.

        > На современных плюсах легче писать, чем на плюсах 10-летней давности
        Точно берёшь какую нибудь библиотеку и хочешь её собрать и тут выяснятся что она требует только определённую версию компилятора иначе она или не собирается или не проходит тесты. Еще прикольней когда компилятор сообщает что у вас префис Q не поддерживается и 128битные флоаты (которые вы не используете) не работают. Да и раздел depricated постоянно растёт. Если вы пишете на современном C++ оно через пару лет может уже не собираться на новых компиляторах. Современные плюсы это страх и ужас. Вместо того что бы выкатить новый язык они пытаются сделать из говна конфетку сохраняя обратную совместимость с переменным успехом. Вспомните с чего начинался C++. Это был препроцессор языка C. Так вот эти препроцессоров могло быть сколько угодно. И обычные скриптовые языки элементарно справляются с подобной задачей (генерацией из DSL C кода). И в отличии от C++ не надо ждать выхода нового стандарта, что бы получить например обычную рефлексию кода или аспекты.

        >Пхп-код уже можно компилировать под attiny?
        дело не в php. А в инструменте который из коробки позволяет просто решать типовые задачи и разбивать на простые сложные. Что C++ из коробки умеет парсить аргументы командной строки как gargs или читать конфигурационные файлы, или упаковывать логи, работать с изображениями, звуком, сетью, базами данных, печатать на принтер, gui и т.п. даже работа с микрософтовскими COM интерфейсами IDispatch для C++ это грусть, печаль и насилие над здравым смыслом.
        Вместо решения проблем которые возникают при реализации практических задач C++ предоставляет разнообразные инструменты программирования ради программирования.
        Вот например какое отношение к реальным задачам имеет stric aliasing? Даже сам термин UB для стандарта, которого все должны придерживаться, выглядит как издевательство. Вместо нормального языка запросов для анализа и преобразования кода использовать метапрограммирование на плюсовых шаблонах это вообще садизм.


  1. mbait
    21.08.2018 01:17

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

    Что такое «одинаковый префикс» и куда нельзя добавлять выравнивание (в оригинале там «compiler is not free», то есть всё таки можно, но ограниченно)? Я сломал голову, пытаясь понять этот отрывок.


    1. tyomitch
      21.08.2018 11:45

      Имеется в виду, что если структуры начинаются с одинакового набора полей, то эти поля внутри структур будут размещаться одинаково. (В частности, если между ними будет добавлено выравнивание в одной из структур, то во второй будет добавлено в точности такое же выравнивание.) Подробнее: stackoverflow.com/a/19805187


    1. alexxz
      21.08.2018 13:29
      +1

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

      #include <stdio.h>
      #define N 1000
      struct StructA {
          int  a;
          char b;
      };
      
      StructA VectorA[N];
      
      struct StructB {
          int  a [N];
          char b [N];
      };
      
      StructB VectorB;
      
      int main(void)
      {
          printf("VectorA: %lu \n", sizeof(VectorA));
          printf("VectorB: %lu \n", sizeof(VectorB));
      }
      

      VectorA: 8000
      VectorB: 5000
      Как видим, по памяти аккуратней может выйти. Также, например, найти сумму полей a будет быстрее в альтернативной структуре данных. Однако, компилятор не имеет права так поступить.


  1. stalkerg
    21.08.2018 04:51

    Последний абзац про потоки и GPU это бред какойто… очень много алгоритмов невозможны без операций над шаред памятью даже пресловутые lock-free.
    А в GPU вообще всё иначе.


  1. DareDen
    21.08.2018 09:22

    А вот мне интересно, как автор статьи объяснит, что программы, написанные на таком плохом языке C, но работающие на AMD или ARM, теряют сразу несколько уязвимостей из приснопамятных семейств Spectre и Meltdown? Может все же это некоторые производители процессоров заигрались в экстенсивный путь развития?


    1. khim
      21.08.2018 11:54

      А очень просто: на этих самых AMD и ARM однопоточная производительность сильно ниже, чем «у некоторых производителей»… и теперь понятно — почему.

      А «заигрались»… вся индустри заигралась. Процессоры можно сделать в 10 (если не 100 раз) проще, если не пытаться делать вид, что вы работает на «очень-очень быстрой PDP-11»… об чём и статья…


    1. 0xd34df00d
      21.08.2018 16:51

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

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


      1. poxvuibr
        22.08.2018 19:48

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

        А что с защитой, которую встроили в программное обеспечение разработчики? То, что в Хроме и Файрфоксе? Это можно отключить?


        1. MikailBag
          23.08.2018 01:44

          А вы хотите, чтобы JS из рекламы читал память ядра?


  1. Kelt_Rivera
    21.08.2018 11:34

    Не совсем понимаю, почему тут GPU противопоставляется CPU в плане ILP. На GPU точно так же есть ILP, достаточно глянуть даже на эту старую презентацию по CUDA 10-го года, чтобы убедиться в этом: Better Performance at Lower Occupancy by Vasily Volkov


  1. fukkit
    21.08.2018 14:54

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


    1. saboteur_kiev
      21.08.2018 16:11
      +1

      Си — просто высокоуровневый язык программирования.

      Его уровень может быть можно мерять относительно других языков программирования, например с java/python/javascript.

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


  1. stanislavskijvlad
    21.08.2018 16:38

    Не сочтите за сарказм или иронию, а можно сюда список реально низкоуровневых языков? P.S. Я – интересующийся юзер. В начале было сказано, что ассемблер тоже не подходит.


    1. serbod
      21.08.2018 16:57

      Там же сказано, что хитрожопому CPU твой ассемблер все равно что JavaScript, он его по-своему интерпретирует.


    1. GREGOR_812
      21.08.2018 18:23

      Verilog))) других вариантов в голову не приходит


      1. Dima_Sharihin
        21.08.2018 18:27

        Скорее не верилог, а результат синтеза из его исходников


        1. GREGOR_812
          21.08.2018 19:48

          Так речь ведь о языке шла


      1. stanislavskijvlad
        21.08.2018 22:09

        А brainfuck? )


  1. Karpion
    21.08.2018 22:06

    Что такое язык низкого уровня?
    Ну, я бы определил уровень языка по тому, сколько сил надо для портирования программ на другую платформу. Ассемблер — низкий, т.к. требует много работы при портировании на другой процессор. ABI для работы с периферией («программирование контроллеров») — пожалуй, ещё более низкоуровневый, т.к. требует много работы при смене периферии (например, при смене сетевой карты).

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

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

    Легко доказать, что С был низкоуровневым языком в PDP-11.
    Ну так большинство языков того периода — были низкоуровневыми языками платформы, на которой разрабатывались. Например, в Фортране был «условный арифметический оператор» — явная калька с ассемблерной команды.

    Ключевая причина появления уязвимостей Spectre и Meltdown в том, что создатели процессоров не просто делали быстрые процессоры, а делали быстрые процессоры с интерфейсом PDP-11.
    Чтобы доказать это — надо привести пример более другого процессора, на котором хорошо исполняется более другой язык программирования. Ну и оценить перспективу применения этого процессора с традиционными (широко распространёнными) языками программирования; и этого языка программирования с другими процессорами. Прикинуть стоимость портирования на этот процессор существующих операционок и программ.

    Лично мне кажется, что гораздо более неприятную проблему представляет необходимость выполнять программы, скомпилированные для *86-й архитектуры, и прежде всего — Windows, которую MicroSoft сама не портирует на другие процессоры и другим не позволяет.

    Это значительно усложняет процессоры и приводит к увеличению потребления энергии, но позволяет программистам писать по большей части последовательный код.
    Т.е. альтернатива — это приучить программистов писать непоследовательный код. Интересно, а сколько программистов, готовых писать такой код, есть в мире; желательно дешёвых индусов? Если их не хватает — то сколько времени и денег потребуется для подготовки таких программистов? А есть ли готовые учителя и учебники для обучения программистов, опять же желательно индусов?
    (Если кому-то не ясно про индусов — корпорации предпочитают именно их. Эту тенденцию можно переломить — но тогда давайте оценку стоимости этого перелома).)

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

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

    Эти выброшенные данные имеют видимые косвенные результаты
    Ужасная формулировка.
    1) Косвенные результаты — имеют не выброшенные данные, а совершённые действия (вычисления), которые оказались ненужными.
    2) Косвенные результаты всё-таки являюся скрытыми; но при этом их можно разглядеть.

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

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

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

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

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

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

    во FreeBSD реализация malloc() сообщает системе, что страницы более не задействованы, а система использует первую запись в страницу как доказательство того, что это не так
    Это что вообще значит?

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

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

    Если вы заполните эту структуру нулями и потом укажете значение некоторым полям, будут ли в битах выравнивания нули?
    1) Как кименно я заполню структуру нулями?
    2) Обращаться к битам выравнивания — запрещено. А если туда не обращаться — то их содержимое неважно.

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

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

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

    Есть множество примеров архитектур, которые не были сфокусированы на традиционном коде C и из которых можно черпать вдохновение. Например, такие ориентированные на многопоточность процессоры, как Sun/Oracle UltraSPARC Tx, не требуют столько кеша, чтобы держать занятыми свои исполнительные устройства.
    1) Что такое «традиционный код C»?
    2) Почему Вы считаете, что архитектура *86 фокусировалась именно на C? Мой опыт изучения истории *86 говорит о том, что она формировалась в первую очередь ради совместимости; а C и типа того были не столь важны, ибо в начале существования *86 C не играл на ней особо важной роли.
    3) Мне кажется, что главное отличие Sparc — в большом количестве регистров и в наличии регистрового файла.

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

    Проблема же состоит в том, что программы на C обычно имеют очень мало потоков.
    А что мешает программисту вызывать подпрограммы (процедуры, функции) с указанием «эту подпрограмму можно выполнять в отдельном потоке»?

    Использовать это в C сложно, потому что автовекторизатор должен рассчитать параллелизм на основании циклов в коде.
    А где просто — в Паскале, в Фортране, в Питоне?

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

    Я полагаю, что производителям вообще надо смотреть в сторону архитектуры «у каждого процессора — своя собственная память». При этом между процессорами д.б. очень быстрая шина — примерно как PCI-X по скорости, но без её глупых заморочек.

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

    В сфере разработки программного обеспечения есть миф о том, что параллельное программирование — это сложно.
    Да вопрос даже не в сложности. А в существовании массы готовых программ и алгоритмов (алгоритм — это как бы неготовая программа), написанных в парадигме последовательного программирования. И переделать это всё на параллельное программирование — реально сложно.

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


    1. 0xd34df00d
      21.08.2018 22:28

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

      При этом в предыдущем абзаце вы предлагаете зафиксировать количество регистров.


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

      Достаточно нового бекенда, а при том же наборе команд — так вообще всего лишь нового шедулера регистров, механизма выбора инструкций и так далее. И внезапно это есть, в gcc/clang называется, например, -march.


      А в каом языке это не так?

      В любом функциональном я могу написать map. А потом практически механически заменить на parMap rdeepseq и получить параллелизм. А компилятор и векторизовать может.


      Ну и что? Цыкл внутри условия всё равно не выполняется!

      Но она считывается в условии, а это уже UB.


      Обращаться к битам выравнивания — запрещено. А если туда не обращаться — то их содержимое неважно.

      Правда? memcpy и memcmp делать тоже запрещено?


      А разве так разрешается делать?

      Стандарт в этом месте не очень понятен, насколько я понимаю.


      А можно пример?

      Можно.


      А что мешает программисту вызывать подпрограммы (процедуры, функции) с указанием «эту подпрограмму можно выполнять в отдельном потоке»?

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


      1. khim
        21.08.2018 23:11

        Стандарт в этом месте не очень понятен, насколько я понимаю.
        Достаточно понятен:
        The following type designates a signed integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:
        intptr_t
        The following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:
        uintptr_t
        These types are optional.
        Если типа, куда «влазит» указатель нет — тогда нет и соотвествующих типа intptr_t/uintptr_t, но если они есть… то да, можно преобразовывать. На этом разработчики E2K обожшлись, так как эта «фича» всё тегирование им поломала…


        1. 0xd34df00d
          21.08.2018 23:23

          Я на немножко другой вопрос отвечал: можно ли считать таковые указатели разными. И, как я понимаю, тут отсылка к DR 260.


    1. 0xd34df00d
      21.08.2018 22:36

      Случайно в предыдущем комменте нажал Ctrl+Enter, так что вот ещё порция ответов.


      А где просто — в Паскале, в Фортране, в Питоне?

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


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

      Кто, где? Почему обычно? Откуда статистика?


      Я обычно на всех 12-16-48 ядрах запускаю одну считалку, которая просто запускает нужное количество потоков. А виртуалки я запускаю в среднем раз в год на 15 минут.


    1. khim
      21.08.2018 23:03

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

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

      Лично мне кажется, что гораздо более неприятную проблему представляет необходимость выполнять программы, скомпилированные для *86-й архитектуры, и прежде всего — Windows, которую MicroSoft сама не портирует на другие процессоры и другим не позволяет.
      Проблема курицы и яйцы, на самом деле. Windows за время своего существования была портирована на полдюжины платформ точно. Только никому это оказалось не нужно, потому что программы все скомпилированы под x86.

      В процессоре ARM есть возможность сделать любую команду условный — т.е. резко снизить количество условных переходов.
      В AArch64 уже нет.

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

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

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

      2) Обращаться к битам выравнивания — запрещено. А если туда не обращаться — то их содержимое неважно.
      Как это запрещено? Вы всегда можете сделать memcpy!

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

      А разве так разрешается делать?
      Есть в реализации есть опциональный тип intptr_t — то да.

      Компиляторы могут решить, что два указателя на результаты malloc() при сравнении всегда дают отрицательный результат, даже если они указывают на один и тот же адрес.
      А можно пример?
      Примерно что-то подобное недавно обсуждавшемуся. Хотя конкретно с malloc'ом у меня не получилось.

      3) Мне кажется, что главное отличие Sparc — в большом количестве регистров и в наличии регистрового файла.
      Он говорит не про Sparc, а про Ниагару. Где отсутствие всяких спекуляций компенсируется тем, что каждое ядро исполняет 4 потока. POWER9 умеет 8 потоков на ядро исполнять.

      Исследовательские процессоры расширили этот концепт до очень большого количества аппаратно-планируемых потоков. Ключевая идея состоит в том, что с достаточным количеством потоков процессор может приостановить те потоки, которые ожидают данных, и наполнить исполнительные устройства инструкциями из других потоков.
      Это же HyperThreading!
      Да, но HyperThreading — это очень ограниченный вариант. Речь идёт о всяких процессорах, исполняющих сотни потоков в параллель.

      Проблема же состоит в том, что программы на C обычно имеют очень мало потоков.
      А что мешает программисту вызывать подпрограммы (процедуры, функции) с указанием «эту подпрограмму можно выполнять в отдельном потоке»?
      То что процессоры имеют мало потоков, в основном. Да — это проблема курицы и яйца классическая…

      Использовать это в C сложно, потому что автовекторизатор должен рассчитать параллелизм на основании циклов в коде.
      А где просто — в Паскале, в Фортране, в Питоне?
      Хотя Occam, хоть Haskell. Да и даже, отчасти, Go.

      Протокол поддержки когерентности кеша — одна из сложнейших составляющих современного процессора.

      Это — проблема не процессора, а общей архитектуры компьютера.

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

      Я полагаю, что производителям вообще надо смотреть в сторону архитектуры «у каждого процессора — своя собственная память». При этом между процессорами д.б. очень быстрая шина — примерно как PCI-X по скорости, но без её глупых заморочек.
      Infiniband плюс GPGPU, да. Типичная прхитектура суперкомпьютеров сегодня. Но программы на C там не запустить.

      В сфере разработки программного обеспечения есть миф о том, что параллельное программирование — это сложно.
      Да вопрос даже не в сложности. А в существовании массы готовых программ и алгоритмов (алгоритм — это как бы неготовая программа), написанных в парадигме последовательного программирования. И переделать это всё на параллельное программирование — реально сложно.
      Однако выбора-то и нету! Иначе дальнейшее увеличение количества транзисторов окажется бессмысленным.

      Алан Кэй был бы очень удивлён, услышав это: он научил детей использовать модель акторов, с помощью которой они писали программы на более чем 200 потоков.
      Замечательно. А что делают эти программы? Может, там есть какие-то новые алгоритмы?
      Написать программу на 200 потоков — мало. Надо, чтобы эта программа ещё и делала что-то полезное. Ну, хорошо бы написать офисную программу (Word, Excel), графический редактор (PhotoShop), браузер или Web-сервер — и сравнить с существующими. Даже не обязательно воспроизводить полностью функциональность — хотя бы основную.
      И снова проблема курицы и яйца: подобная программа на существующем железе будет медленнее того, что у нас есть… а другое железо без соответствующих программ «не взлетит».

      Основная проблема — как раз даже не в C, а в JavaScript: это убожество было изначально заточено под однопоточное исполнение и так просто от этого не отойти. Очень забавно что такой «типа высокоуровневый» язык оказался де-факто более «низкоуровневым», чем C…


      1. 0xd34df00d
        21.08.2018 23:31

        Хотя конкретно с malloc'ом у меня не получилось.

        Посмотрите мою ссылку в комментарии выше. То, что компилятор вырезает сравнение p и q и сворачивает это в константу 0, ИМХО, вполне себе доказательство.


        Хотя Occam, хоть Haskell.

        В хаскеле, кстати, с этим чуть сложнее, чем просто автовекторизовать произвольный map, потому что нужно делать strictness analysis. Упрощая,


        foos :: [Int]
        foos = [1, 2, 3, undefined]
        
        bars :: [Int]
        bars = (* 2) <$> bars
        
        main :: IO ()
        main = print $ take 3 bars

        невекторизованная программа спокойно выведет три инта. Векторизованная программа с шириной векторизации в 2 или 4 инта напорется на undefined.


        Но это проблемы семантики вычислений.


      1. tyomitch
        22.08.2018 15:04

        В процессоре ARM есть возможность сделать любую команду условный — т.е. резко снизить количество условных переходов.
        В AArch64 уже нет.

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


    1. qw1
      22.08.2018 12:44

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

      То есть, чтение памяти, выданной malloc, без записи в неё, это UB, и запросто может вызвать page fault, если выделение физических страниц привязано к записи по адресу, а к чтению не привязано.


      1. Karpion
        22.08.2018 16:45

        Ну, я до сих пор полагал, что это делается немного совсем иначе:


        • В системе есть одна реальная страница памяти, забитая нулями. Она выделяется задачам только на чтение, поэтому она всегда остаётся такой "занулённой".
        • Когда задача просит у ядра выделить ей память — ядро выделяет ей эту страницу. Т.е. в таблице страничных дескрипторов проставляются ссылки на эту страницу — как я сказал выше, все ссылки разрешают только чтение (в дескрипторе страницы — содержится её адрес и права доступа).
        • Теперь при чтении — мы получаем нули. Заодно облегчается работа кэша.
        • А вот при первой же операции записи — происходит exception, и ядро понимает, что память надо выделить реально.

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


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


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


        1. qw1
          22.08.2018 17:26

          Вы слишком уходите в практические аспекты.

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

          Хотя, схема не имеет дыр. Можно при попытке записи в страницу подставлять на этот адрес новую занулённую страницу из пула страниц (а чтение будет fault-ить). Не вижу преимуществ к тому, чтобы сразу после malloc держать там занулённую read-only страницу.


          1. khim
            22.08.2018 19:21
            +1

            Пример этот был дан, чтобы продемонстрировать ситуацию, когда чтение перед записью, являясь UB, действительно может привести к неожиданным последствиям, даже к page fault.
            Быстро поднятое не считается упавшим. Чем этот page fault будет отличаться от page fault'а, который произойдёт если стараницу вытеснили на диск? Сигнал в приложение же не поступит!


            1. qw1
              22.08.2018 21:23

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


              1. khim
                22.08.2018 21:47

                А! Понял наконец. Да. Теоретически такое возможно. Практически — так никто не делает.


        1. tyomitch
          22.08.2018 18:06

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


          1. Karpion
            22.08.2018 21:09

            Никакая библиотека, работающая в user-space, не может заблокировать страницу памяти от записи — для этого надо обращаться в ядро.

            Я также не вижу смысля тянуть с обнулением страниц.

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


            1. tyomitch
              22.08.2018 21:14
              +1

              Никакая библиотека, работающая в user-space, не может заблокировать страницу памяти от записи — для этого надо обращаться в ядро.
              Да, надо. И что ей мешает?
              Я также не вижу смысля тянуть с обнулением страниц.
              Экономия времени в случае, если выделенная память будет использоваться не вся.
              И наконец, библиотека должна оперировать малыми кусками памяти, а не страницами.
              У ОС она всё равно запрашивает память страницами, значит и внутренний учёт страниц должна вести.


              1. Karpion
                22.08.2018 22:25
                -1

                Допустим, программа говорит free(), а потом malloc(). Обе операции обрабатывает библиотека. На операции free() — библиотека может обратиться в ядро и отдать ему эту память. Ну а может обратиться в ядро с просьбой пометить эту память как ro. Я как-то не вижу особой разницы в трудоёмкости.

                «Экономия времени в случае, если выделенная память будет использоваться не вся.» — тоже очень сомнительно. Ибо обработка exception — довольно дорогая.
                Вот экономия памяти — это да, интересно.


                1. khim
                  23.08.2018 00:15

                  На самом деле всё ещё хуже: дорого не только exception обработать, дорого и права доступа менять тоже.


            1. khim
              22.08.2018 21:52

              Всё мимо. Настоящая причина, по которой так никто не делает — это куча проблем при попытке помирить требования POSIX на работу функции signal, и такую вот «незаметную» обработку SIGSEGV.

              Если же говорить не про библиотеку, а про программу — то так очень даже делают. В частности небезивестный Bourne shell так делает (не bash, а именно оригинальный, который тот самый Стефан написал), некоторые лисп-системы так делают…


      1. DistortNeo
        22.08.2018 18:04
        +1

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


  1. grigorym
    22.08.2018 10:01

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