В статье [ссылка] было заявлено, что производительность Haskell кода превзошла код на С++. Что сразу вызвало интерес, т.к. и то и другое может генерироваться LLVM компилятором, значит либо Наskell может давать больше хинтов компилятору, либо что-то не так с С++ реализацией. Далее мы разберём, как череда случайностей в действиях автора привела к неправильным выводам, которые описываются таблицей ниже (под катом).
Предисловие
Недавно на хабре появилась очередная статья от 0xd34df00d про оптимизацию хаскель кода. Сравнивается в таких случаях естественно с неоспоримым лидером в производительности — С/C++. Затем последовал разбор этой статьи от yleo о том какой асм код действительно лучше, и в чём кроется различие реализаций на разных ЯП (рекомендую к прочтению). Ещё раньше (около полутора месяцев назад), была опубликована предыдущая статья из серии "Хаскель vs С/C++", и я проделал похожий разбор, но вместо того чтобы опубликовать его на Хабр — увы, отложил в долгий ящик. Бурные дискуссии в комментариях на этой неделе побудили меня вернуться к прошлой теме. Сегодня я его наконец-то достал тот markdown документ из ящика, стряхнул пыль, дописал, и предоставляю его на ваше обозрение.
Введение
Напомню, что задача была про подсчёт расстояния Левенштейна [вики], и вот такие результаты были показаны в оригинальной статье:
Реализация | Отн. время |
---|---|
С clang 9 | 103% |
С gcc 9.2 | 125% |
C++ gcc 9.2 | 163% |
C++ clang 9 | 323% |
Остановимся только на С/С++, т.к. другие бенчмарки были написаны, как заметили в комментариях, по методу "Пишем одной рукой, иногда закрывая надолго глаза". Они были добавлены как "бонус", и в рамках одной статьи их полноценно разобрать невозможно. Тем не менее, всё равно выражаю большой респект тому человеку, который в одиночку написал реализации больше, чем на 10-ке языков.
Что подозрительно
Во-первых, сразу бросается в глаза, что С++ версия гораздо медленнее Си, что, на самом деле, странно. Далее мы найдём, где потерялся zero-cost, а в другой части, надеюсь, покажем, как именно можно использовать мета-программирование С++, чтобы обходить Си. К тому же, на clang плюсовая версия оказалась медленнее в 3 (!) раза, хотя сишный код почти такой же по скорости как хаскель+ллвм, что ожидаемо, т.к. сlang и llvm — это один проект.
Череда случайностей
Если проследить, то дело было так: автор написал наивный код на плюсах и скомпилировал gcc и clang. Последний оказался в два раза медленнее, и автор его отбросил. Далее он проделал пару попыток оптимизировать код (подробнее ниже), но gcc было абсолютно фиолетово на эти изменения. После этого автор принялся за Хаскель и написал код, который делает примерно то же самое, что и плюсовый, за исключением неких, как окажется потом, важных перестановок инструкций.
Дьявол кроется в деталях
Нюанс std::min({...})
Деталь номер один, которую заметил сам автор и множество людей в комментах, это использование std::min.
Мне таки удалось воспроизвести ускорение в случае C++.
Так, если вместоstd::min({delCost, insCost, substCost})
написатьstd::min(substCost, std::min(delCost, insCost))
,
то время работы под clang — уменьшается до 0.840 секунд
Ура, быстрее всех остальных вариантов и почти хаскель.
(Автор оригинальной статьи — 0xd34df00d)
Смотрим на хаскель версию:
A.unsafeWrite v1 (j + 1) $ min (substCost + substCostBase) $ 1 + min delCost insCost
Как ни странно, тут как раз и есть два раза вызов функции min
от двух аргументов и в том же порядке! (надеюсь, на таком уровне я понимаю хаскель правильно). Таким образом, автор, после исправления C++ версии, сам получает, что один llvm равен второму llvm. Собственно, это я ожидал с самого начала. К сожалению, предположение, что "Наskell может давать больше хинтов компилятору" не подтвердилось. Но судьба сложилась так, что изначально автор статьи "Быстрее чем С++; медленне, чем PHP" проверил эту замену только на гцц, а этому компилятору от этого ни холодно, ни жарко. Как видно в бенчмарке ниже:
Компилятор | Время оригнала | Время исправленного (1) |
---|---|---|
haskell/llvm | 910ms | - |
gcc 9.2 | 1211ms | 1211ms |
clang 9 | 1915ms | 852ms |
В реализации stdlib от gcc я не нашел каких-то специализаций для std::min
в случае трёх элементов, хотя это не должно быть проблемой сделать на С++. На данный момент минимум там находится путём создания массива на стеке и его обхода алгоритмом std::min_element
. В простых случаях, как уже было замечено в комментариях, разницы нет, и компилятор умеет сам выкидывать массив на стеке и генерировать оптимальный код:
f(int, int, int):
cmp esi, edi
mov eax, edx
cmovg esi, edi
cmp esi, edx
cmovle eax, esi
ret
Примечание: cmov*
= conditional move (условие: g
— greater, le
— less equal, и т.д.).
Но что интересно, в случае, когда вовлечены указатели, это не так, и clang, в отличии от gcc, зачем-то кладёт данные на стек (туда указывает rsp регистр):
fptr(int*, int*, int*):
mov eax, dword ptr [rdi]
mov dword ptr [rsp - 12], eax
mov ecx, dword ptr [rsi]
mov dword ptr [rsp - 8], ecx
cmp ecx, eax
cmovle eax, ecx
mov ecx, dword ptr [rdx]
cmp ecx, eax
cmovle eax, ecx
ret
Что косвенно объясняет, почему clang более чувствителен к этому изменению в исходном коде. Также в случае без initializer_list
компилятор генерирует оптимальный asm уже при -O1
, а с ним нужно вовлечь больше оптимизаций (-O2
), чтобы добиться того же asm выхлопа. Таким образом, std::min(std::initializer_list)
не совсем зеро-кост, тут создателям стд-либы, возможно, стоит подумать над перегрузками для некоторых эвристик.
Вынесите s1[i]
Деталь номер два, которую я обнаружил — это другая оптимизация из Хаскель, которую потеряли в С++.
Да вынесите вы наконец уже s1[i]
за рамки цикла! (я)
Опять же, по роковой случайности, гцц на неё почти по барабану, а автор-то тестировал на гцц и, соответственно, забыл внести её в итоговое сравнение. Итак, это вынос s1[i]
за тело цикла, который присутствовал уже на нулевой итерации хаскель кода
let s1char = s1 `BS.index` i
let go j | j == n = pure ()
-- Тело цикла
После применения этой оптимизации к коду на С++, мы получаем результаты быстрее, чем хаскель при сборке компилятором clang. Т.е. хаскель + llvm всё же добавляет какой-то оверхед, или ему не хватает -march=native
. Самое забавное то, что, оставив строку с std::min
без изменений, эта версия работает быстрее, чем если применить оба изменения! Значит, компилятор как-то не очень предсказуемо переставляет инструкции во время оптимизации, и некоторые решения "волей случая" оказываются быстрее, но это мы обсудим подробнее дальше.
Компилятор | Оригинал | Исправленный (2) | Оба исправления (3) |
---|---|---|---|
haskell/llvm | 910ms | - | - |
gcc 9.2 | 1211ms | 1195ms | 1195ms |
clang 9 | 1915ms | 742ms | 831ms |
Допилить напильником
C++ вариант приведен для сравнения.
Его можно оптимизировать ещё немного,
но тогда это получится C с плюсовым main'ом, что не так интересно.
(Автор оригинальной статьи)
Как мы уже увидели, даже маленькие исправления могут быть абсолютно непредсказуемы в зависимости от компилятора, Поэтому, я всё же попробую чуть-чуть допилить код, потому что это не сложно:
size_t lev_dist(const std::string &s1, const std::string &s2) {
const auto m = s1.size();
const auto n = s2.size();
std::vector<int> v0;
v0.resize(n + 1);
std::iota(v0.begin(), v0.end(), 0);
auto v1 = v0;
for (size_t i = 0; i < m; ++i) {
v1[0] = i + 1;
char c1 = s1[i];
for (size_t j = 0; j < n; ++j) {
auto substCost = c1 == s2[j] ? v0[j] : (v0[j] + 1);
v1[j + 1] = std::min(substCost, std::min(v0[j + 1], v1[j]) + 1);
}
std::swap(v0, v1);
}
return v0[n];
}
Тут я перешёл на 32-битный int
в векторе и чуть упростил подсчёт результата — сначала ищем минимум, потом инкремент (что опять же уже присутствует в хаскель коде).
Компилятор | Оригинал | Допиленный (3b) |
---|---|---|
haskell/llvm | 910ms | - |
gcc 9.2 | 1210ms | 831ms |
clang 9 | 1915ms | 741ms |
Ура, теперь GCC тоже ускорился. Также я пробовал заменить счётчик j
на указатели, что внезапно замедлило GCC. В то же время скорость clang осталась на своём максимуме.
Осознаем результаты
LLVM == LLVM
Во-первых, мы получили, что если написать С++ код так же как Haskell код, то результат одинаковый при использовании clang-9. К тому же, на моём процессоре Skylake C++ версия оказывается даже быстрее. Код, который я бенчмаркал, будет находится на гитхабе, и можно будет проверить данный тезис на архитектуре Haswell, которую в основном использовал автор.
Итак, приходим к выводу, что вместо сравнений языков, по факту проводилось сравнение компилятора GCC и LLVM.
В оригинальной статье было детально разобрано, как на хаскеле написать код, заточенный под llvm, и обойтись без ffi, за что автору спасибо.
GCC vs CLANG
Во-вторых, до этого момента сложилось впечатление, что старичок gcc уже ни на что не годится. Поэтому, ставьте свою генту пересобираться clang-ом и читайте дальше.
Отдельно отмечу, что в Си версии исходного кода, предоставленной автором, была директива компилятора, которая выбирает лучший код в зависимости от компилятора (#if !defined(__GNUC__) || defined(__llvm__)
), что объясняет относительную разницу между Си реализациями и С++ реализациями, и соответственно, делать выводы о соотношении Си и С++ по приведённой автором таблице нельзя.
clang не осиливает (либо не пытается) убрать ветвления. (Голос из зала)
Попробуем понять, чем вызвана разница между GCC и LLVM. Для этого посмотрим, что там наворотил компилятор в asm. С gcc все более-менее ясно: один внутренний цикл, который на основе команд cmov* делает min (аналогично тому, что мы видели листинге выше). Я беру версию (3), это та, что с двумя исправлениями, и С++ выглядит так:
for (size_t j = 0; j < n; ++j) {
auto delCost = v0[j + 1] + 1;
auto insCost = v1[j] + 1;
auto substCost = c1 == s2[j] ? v0[j] : (v0[j] + 1);
v1[j + 1] = std::min(substCost, std::min(delCost, insCost));
}
Ассемблер, который я ради не сильно знакомых с ним читателей решил прокомментировать, получается таким:
.L42:
inc rcx // j++
mov rdi, QWORD PTR [r12+rcx*8] // загрузить v0[j+1]
xor edx, edx // обнулить %edx
cmp r10b, BYTE PTR [r11-1+rcx] // c1 == s2[j]
setne dl // результат в последнем байте %rdx
lea r9, [rdi+1] // стало v0[j+1] + 1
add rdx, QWORD PTR [r12-8+rcx*8] // добавить v0[j]
lea rsi, [rax+1] // %rax это v1[j]
cmp rdi, rax // сравнить v0[j+1] и v1[j] до += 1
mov rax, r9
cmovg rax, rsi // на основе сравнения выбрать результат после += 1
cmp rax, rdx // меньшее %rax, %rdx
cmovg rax, rdx
mov QWORD PTR [r8+rcx*8], rax // v1[j+1] = ...
cmp rbx, rcx // loop
jne .L42
Тут компилятор сам сделал оптимизацию, которая упоминалась в оригинальной статье — вместо загрузки v1[j]
на каждой итерации мы передаем его через %rax
.
Что же касается LLVM, то тут какая-то лапша из переходов, которую полностью приводить не буду. Отмечу лишь для примера, что во одном из кусков во внутреннем цикле имеется конструкция, частично похожая на предыдущую:
.LBB1_40: # in Loop: Header=BB1_36 Depth=2
mov qword ptr [r14 + 8*rsi + 8], rax
mov rdx, qword ptr [rbx + 8*rsi + 16]
inc rdx
inc rax
xor ebp, ebp
cmp cl, byte ptr [r13 + rsi + 1]
setne bpl
add rbp, qword ptr [rbx + 8*rsi + 8]
cmp rax, rdx
jg .LBB1_41
lea rdi, [rsi + 2]
cmp rax, rbp
jle .LBB1_44
jmp .LBB1_43
Примечание: jmp, j*
= jump (условие: jg
— greater, jle
— less equal, и т.д.).
Тоже загружаем данные из v0[j+1]
, v0[j]
, делаем cmp
для s1[i]
, но потом у нас идёт набор из cmp + jump во всех вариациях. Оставшуюся лапшу так детально комментировать не буду, но вполне ожидаемо, что на однотипных данных (а это то, что делал автор) бранч предиктор рулит, и такой код работает быстрее, как заметили в комментариях. Давайте попробуем на других данных — двух случайных строках.
Компилятор | str a — str a, str a — str b | random-random x2 |
---|---|---|
gcc 9.2 | 1190 ms | 1190 ms |
clang 9 | 837 ms | 1662 ms |
Как и ожидалось, в GCC не меняется результат ни на одну миллисекунду, а LLVM замедляется в 2 (!) раза, потому что бранч предиктор больше не работает.
Итак, приходим к основному тезису статьи. По факту были сравнены две реализации алгоритмов: одна основана на условных переходах (jump), другая — на операциях условного копирования (cmov).
Одна реализация работает лучше на однотипных данных, другая — на случайных.
Естественно, компилятор не может знать заранее, какие данные будут у программы в реальной жизни. Для того, чтобы решить эту задачу, существует PGO (Кстати, тут языки с JIT могут заиграть новыми красками). Я проверил это в нашем случае и получил, что GCC после PGO выдает результат наравне с самой быстрой версией clang. Какие данные ближе к реальной задаче — это предмет отдельной дискуссии, которую мы оставим для последующих изысканий. Мне кажется, что хоть мы и будем в реальности сравнивать близкие строки, выбор в алгоритме между удалить/заменить/вставить всё же будет случайный, а ветка, когда не надо делать ничего, может быть обработана отдельной эвристикой.
Выводы
- Бенчмарки порождают холивары, а холивары — новые бенчмарки
- Никакой дискриминации нет, LLVM генерирует хороший код для всех
- Мало того, что GCC и LLVM дают разную скорость в зависимости от задачи, так еще и в зависимости от входных данных
- Бенчмарки без четкого технического задания и полноценного набора входных тестовых данных не имеют смысла
- Автору надо прикручивать обратно ffi. На самом деле нет, т.к. у него есть другой алгоритм, о чём, надеюсь, узнаем в других сериях
- Не спешите бежать на новый язык или компилятор на основе бенчмарков в интернете
Надеюсь, в следующей части будут рассмотрены детально эти два подхода и разобраны их плюсы и минусы в более реальных ситуациях, а так же предложены способы ускорения этого алгоритма.
Где я мог обмануть
Для полной корректности выводов надо было провести ещё и следующий эксперимент: Убирать оптимизации из Хаскель версии и проверять, стало ли оно медленнее, тогда можно было бы более полно проверить тезис об "умности" компиляторов и, в частности, о влиянии алиасинга. Но эту задачку я оставлю любителям ФП или Rust (Блин, я же сам в числе последних).
P.S. Альтернативное решение
Первый способ решить задачу — это проверить, решил ли её уже кто-то другой
(Мой препод по матану)
Напомню, что задача — это поиск редакционного расстояния, т.е. минимального числа вставок, удалений и замен символов, которые надо произвести, чтобы из строки s1 получить строку s2. Статья об этом уже была на хабре. В данной заметке мы рассмотрели способы оптимальной реализации алгоритма Вагнера-Фишера, который требует O(n*m) времени (два вложенных for). По ссылке выше есть ещё алгоритм Хиршберга, но он тоже работает за квадратичное время. Хотя всё же можно ускорить алгоритм, если нас не интересуют расстояния больше некоторого наперёд заданного k. Так же, есть трюк который должен позволить сделать векторизацию. Об этом писал автор обсуждаемой здесь статьи, но это уже тема для другой заметки.
Спасибо LinearLeopard за исправление ошибки в этом абзаце.
Приложение
Методика бенчей
- Флаги компилятора:
-O3 -march=native -std=gnu++17
. - Процессор: Intel i5-8250U (да, ноут)
- ОС: Ubuntu 19.04 / Linux 5.0.0
- Первый прогон для разгона турбо-буст, далее берём минимум из пяти подряд. Отклонения в рамках 1-2%.
- Между запусками разных реализаций 1с прохлаждаемся (да, ноут)
Добавлено: скрипты для ленивых
Можете запустить всё то же самое на своем железе и сообщить общественности результат: ссылка на гитхаб.
Добавлено: Результаты без -march=native
По заявкам в комментариях, решил проверить влияние этого флага.
Флаги | -O3 -march=native | -O3 -march=native | -O3 | -O3 |
---|---|---|---|---|
Компилятор | Оригинал | Допиленный (3b) | Оригинал | Допиленный (3b) |
haskell/llvm | - | - | 910ms | - |
gcc 9.2 | 1210ms | 831ms | 1191ms | 791ms |
clang 9 | 1915ms | 741ms | 1924ms | 807ms |
0xd34df00d
Ух, моар «разборов».
Какое милое КПДВ. А можно до конца цитировать плз?
«Если сравнивать с clang (что вроде как логичнее), то всё становится ещё хуже для плюсов: почему-то на этой задаче clang проигрывает GCC в пару раз, и разница становится не 40%, а этак раза три. Впрочем, одна маленькая модификация C++-кода это поменяет.»
Вполне может. Алиасинга того же нет, да и вообще язык строже.
А далее (на самом деле в самом начале) автор даже упомянул, что кое-какие изменения в коде достаточно ускоряют clang.
Это не исправление. Если вы таки посмотрите на дизасм
std::min({a, b, c})
, то увидите, что он разворачивается в ту же конструкцию, что иstd::min(a, std::min(b, c))
с точностью до порядка аргументов — initializer list'ы призваны быть дешёвыми и реализуемыми без лишних аллокаций памяти.И да, это не «один llvm равен второму llvm». Тут очень важно, что делает фронтенд компилятора, и как именно он разворачивает синтаксический сахар. Например, у вас может возникнуть желание написать
minimum [a, b, c]
в хаскеле, но ввиду семантики списков это совсем неэквивалентная замена и куда ближе кstd::vector vec { a, b, c }; std::min_element(vec.begin(), vec.end());
Хотя, конечно, можно сказать, что компилятор обязан развернуть свёртку по списку статически известной длины в явную последовательность приложений соответствующей функции, и мне, если честно, немного печально, что ghc сегодня этого не делает (хотя это легко починить самому через
RULES
-прагму, но это уже немного читерство, ИМХО). Непонятно, правда, причём тут LLVM, если это дело фронтенда.См. выше, хинты кодгену тут ни при чём.
В общем случае на результат работы компилятора C++ без оптимизаций лучше не смотреть, и
std::min
тут не исключение.Ну, то есть, clang не смог задешугарить предназначенный ровно для таких ситуаций сахар?
Это (как ЕМНИП писал khim) скорее бага в clang.
А перегрузку по длине списка у вас сделать не получится.
Да, там нет
-march=native
. И то, что вы не указали к этому моменту, что вы не используете-march=native
для хаскель-версии, но используете для плюсовых, это печально.Так это и означает, ровно то, что хаскель имеет оверхед, сравнимый с C++, разве нет? Спасибо, что лишний раз доказали это за меня.
Только в данном случае данные совсем не случайные. Искомое расстояние почти всегда считается для очень близких данных.
А я бы побежал, раз оказывается, что от хаскеля в подобных задачах околонулевой оверхед, и различные реализации C++ между собой больше различаются, чем хаскель и быстрейшая реализация на плюсах.
Оптимизации кода на хаскеле, которые представлены в исходной статье, никак не связаны с алиасингом.
Итого, что бы ты ни делал, тебя обвинят в читерстве или, как минимум, некорректных бенчмарках, потому что выиграли не С. Придерутся даже к тому, что ты не указал в методике измерений, что происходило с прерываниями и какая была другая нагрузка на машину. Если же в твоих бенчмарках выигрывает С, то можно делать что угодно (включая сравнение неэквивалентных алгоритмов, использование старых версий компилятора хаскеля, заточка под конкретную архитектуру кода на C с
-march=native
без указания этого, и так далее), и всё будет в порядке. А если ты типа как нашёл что-то, что выглядит и звучит как объяснение (не имеющее вообще никакого смысла, вроде упомянутого в комментах тезиса о том, чтоstd::min({})
создаёт вектор), то ты, короче, молодец.Не, точно надо про метабенчмарки статью писать.
technic93 Автор
Ох какой длинный комментарий.
Вся цитата на КДПВ не влезла, а если серьезно, то вам не кажется что само предложение уже противоречивое: Код в три раза медленнее или после модификации работает так же, вы определитесь уже? И моя претензия что таблица не обновлена остается в силе :) Да и ваша цитата про модификацию далее по тексту. Вообщем имхо кдпв на то и кдпв чтобы быть провакационным, надеюсь вы не обижаетесь. Хм… возможно я ваши цитаты не обозначил что они именно ваши — исправлю.
В данном случае это не явно не проявилось. С удовольствием посмотрю на других примерах.
Если вы таки прочтете мой пост внимательно…
Вы сами видите что на ллвм min разворачивается в абсолютно разные инструкции в зависимости от окружающего кода. В хаскель коде ведь просто min от двух аргументов, так что вопрос про списки вообще не стоит. Более того вы там переставили порядок аргументов в версии через
{}
и без неё, что тоже важно. К тому же, на gcc можно тоже путем флагов или прагм (не точно) выбрать во что развернется min.march=native дает минимальный прирост, а иногда даже делает хуже я не помню точно, давно было. Тут никакой авто-векторизации нету, так что толку от него имхо мало.
Ну у хаскеля вообще разброс в 10 раз. Но вы можете проверить как он будет себя вести при перестановках порядка сравнения и т.п. Я же предложил это в заключительной части. Я вангую что будет то же разброс в 2-3 раза в зависимости от комбинации данные + код что и у С++.
По поводу всего остального. Я Хаскель упомянул только в первой части, и показал как именно эквивалентный 1в1 код на С++ дает примерно тот же результат на этой задаче. И почему мелочи важны. Далее речь про разные компиляторы и т.п. Кто выиграл не так интересно как почему, и тут ответ как раз кроется в cmov vs jump что и было показано.
0xd34df00d
Эм, а где там написано, что он работает так же?
Их можно сконструировать искусственно. Не знаю, правда, насколько это честно.
И это баг в llvm.
Эм, ну да. Это ЕМНИП давало максимальный профит.
Судя по «давно было», на этой задаче вы не проверяли?
Вон, в спецолимпиаде по
wc
-march=native
даёт клангу двухкратный профит на моей машине (старый уже хазвелл) и трёхкратный на современных -lake'ах с AVX512. Хотя там тоже автовекторизацией в том же стиле, что руками, и не пахнет.Это как? И где вы вообще нашли хотя бы две актуальных реализации хаскеля? :)
technic93 Автор
Так у вас показано в таблице для Си — там 103%. Это и есть так же. Ну и у меня так же.
Там нету ни одного векторного регистра в теле внутреннего цикла, вы мой асм смотрели или нет?)
Это не совсем баг, можно переставить ветвление в разном порядке, каждый будет лучше для определенного паттерна входных данных. Какой он как выбирает не понятно. В хаскеле же вы тоже юзаете эту багофичу. Не ужили я так плохо написал что не донес эту мысль :(
Там у вас их куча на графиках. Мы точно про одну и ту же задачу говорим. Я не про wc если что.
0xd34df00d
Ну да, а без этих
оптимизацийзаточек под компилятор — в три раза хуже.Ну а почему тогда
даже на моей машине, где ещё меньше новых инструкций, чем у вас?
Я про то, что он зачем-то спиллит регистры.
Разная последовательность сравнений — это, конечно же, не баг, это конкретный выбор конкретного компилятора. И да, оптимизировать под это руками — такая себе идея, лучше брать PGO (как и вы написали, как и я написал полтора месяца назад), но PGO — это другое измерение этой задачи.
Я про разные реализации языка, а не алгоритма. Если от лёгкого шевеления исходного кода от становится быстрее на актуальном clang, но медленнее на актуальном gcc (и это то, что я наблюдал, и что указано в исходной статье), то это не оптимизация, а заточка исходника под конкретный компилятор, его решения и их связь (или её отсутствие) с входными данными.
Безусловно, это осмысленно, если вы пишете что-то, что будет считаться неделю, и лишние 1-2-5% на конкретных данных на конкретной системе с конкретным компилятором вполне оправданы, но цель игры с такими вещами при сравнении разных языков от меня ускользает.
technic93 Автор
Ну вот про эту разницу я и прокомментировал что дело в march=native. Можно в этом ковыряться еще но это дает процентов 10, по сравнению с разницей в два раза.
Это была не заточка под компилятор а под данные. Что я продемонстрировал на примере разных данных. В чем смысл сравнения решений, если порядок "==" разный?
0xd34df00d
Ближе к 20 на моей машине, и это разница между «медленнее реализации на хаскеле» и «быстрее реализации на хаскеле».
Когда я с этим игрался, то порядок аргументов, дававший выигрыш для clang, ухудшал время работы при компиляции gcc.
technic93 Автор
Это всё равно заточка под данные. Просто разный компилятор по разному раскрывал min.
Upd: на самом деле там много разных мелочей, тот же +1 до или после сравнения. Например, последний вариант гцц который самый быстрый на "аааа" стороках, тоже проседает на рандомных данных, я думаю потому что там закрался бранчинг. Правильно отпрофайлить бранчинг сложно (см. статью про wc), я показал более подробнее основной момент.
0xd34df00d
Ну вот учёт, как каждый из компиляторов это раскрывает, и есть по факту заточка под компилятор. Потому что вам придётся обмазывать код
#if
'ами на тему того, каким именно компилятором (и, возможно, какой именно версии) вы собираетесь.Или можно просто взять PGO, наконец.
technic93 Автор
Но а какой смысл подгонять решение под компилятор, если на других данных оно замедлится в два раза? Тем более дальше вы говорите про пго.
technic93 Автор
Кто гцц или шланг?
0xd34df00d
Шланг (в показанном вами примере с раскрытием
std::min({ initializer list })
в более сложном контексте).technic93 Автор
Мне кажется с march=native он там делает что то вроде анролла. Может в этом причина?
apro
Но скорее всего с llvm так не поступает. Потому что вот например Rust
попытался передавать где только может "restrict" и llvm сломалось,
пришлось не выпендриваться: https://github.com/rust-lang/rust/issues/54878
technic93 Автор
Да там бывает, еще интереснее, что после обновления ллвм в расте пропали проверки на выход за границы массива. Пофиксили вчера в 1.41.1.
0xd34df00d
Да, поэтому на практике NCG (родной ghc'шный генератор нативного кода) может творить чудеса, когда речь идёт о программировании в каких-то чисто функциональных парадигмах, а LLVM рулит и педалит, когда речь заходит о числодроблении.
Mingun
Простите, а что, при передаче в LLVM массив внезапно забывает, что у него константная длина? Почему и фронтенд, и бекенд не могут оба делать эту работу? Хотя раз она может и должна делатся бекендом, то зачем ее делать фронтенду?
Вообще, все эти срачи про языки становятся надоедливыми. Я не понимаю, в чем их смысл. Начинать с идеоматического кода и потом обмазывать его всякими хаками по основам исследований, а что лучше в данный конкретный момент с данными конкретным компилятором и на данных конкретных данных — это сравнение хитрецы и навыков реализатора, но никак не языков. Сравнение языков будет, если вы дадите задачу реализовать сотне студентов, которые только что прочти "XXX для чайников" и сравните результат.
Проблема в том, чтобы поставить эксперимент, потому что в такой постановке никто не мешает студенту засунуть в середину вычислений код для подсчета котиков в интернете, а введение формальных правил, типа "используйте то" или "не используйте это" — прямое нарушение условий эксперимента. С другой стороны, постановка условия в виде "реализуйте оптимальнейшим образом" обязательно приведет к тому, что часть особо умных прочитают еще что-нибудь по языку и просто спустятся поближе к уровню ассемблера, что и сделали авторы всех обсуждаемых статей. Так что сравнение языков — это будет сравнение наивных подходов на этих языках к реализации задач. Но не забывайте, что наивный подход в каждом языке разный — банальный (но не точный, просто для иллюстрации) пример — использование рекурсии в ФП языках и цикла в императивных. Наивный алгоритм будет диктоваться как задачей, так и общей философией языка. На этой уровне уже начинает влиять то, как именно была построено обучение в "XXX для чайников", так что идеального сравнения никогда не будет — всегда найдется к чему придраться.
Если на языке нельзя, пользуясь приемами, описанными в вводной книжке, которую можно прочитать за вечер, написать близкий к идеальному код — это плохой язык. Все остальное — это уже демонстация опыта и глубоких пониманий конкретных реализаций, что, ИМХО, от языка никак не зависит.
khim
Да, если ты — ублюдочная девочка, борящаяся «за всё хорошоее и против всего плохого» (а в реальности твоих спонсоров волнует чтобы картинка хорошо по TV смотрелась и больше ничего) — то подобные высказывания нормальны.
Если же вы реально хотите чтобы мир стал лучше — то стоит задуматься. Любая сложная задача имеет простое, легкое для понимания — но неправильное решение.
Почему так? Потому что абстракции протекают и потому единственный способ сделать язык «хорошим» в вашем смысле — это сделать его настолько тупым и неэффективным, что это станет неважно. Ну там QuickBasic, Python, JScript, JavaScript в Netscape 2.0 и так далее.
Далее, проходит несколько лет, люди, пользующиеся этим вот всем, замечают что язык у них — как бы «хороший», но очень уж медленный и ресурсоёмкий. Его оснащают разного рода JIT'ами и примочками типа NumPy, язык становится «плохим»… и через несколько лет всё повторяется.
Пожалуйста — не будьте Гретым Туборгом, это ни к чему хорошему не придёт.
Mingun
Когда вы делаете шаг вперед — вы задумываетесь, какие мышцы бедра напрячь, какие силы приложить к каждой кости, какие углы должны быть между костями, учитываете массу надетой сейчас обуви и налипший на ней снег, и тысячу других мелочей, или все же просто берете, и шагаете?
Точно также и с языками — есть те, где вы вынуждены думать о всяких мелочах, которые вот ну совсем никак не должны влиять на конечный результат. О них должен думать компилятор — его затем и придумали. В этих вещах нет и не может быть никакой вариабельности — тупой, как пробка алгоритм, который верен сейчас, был верен сто лет назад и через сто тоже будет верным. Вы же предлагаете постоянно держать эту чушь в голове, делая за компилятор его работу.
А есть те, где это сделано за вас. Ну например. На заре зарождения языков программирования вы вынуждены были нумеровать строки программы, чтобы иметь возможность к ним перейти с помощью GOTO. Даже если вы его никогда не использовали. Затем появились языки, которые выкинули эту чушь из своей грамматики — за строками должен следить компилятор. Стало лучше? Удобнее? Какие-то абстракции протекли от автонумерации строк? Освободились ли головы программистов от необходимости следить, сколько строк занимает программа, искусственно нумеровать строки не через 1, а через 10, или 100, или любое другое число, чтобы иметь возможность вставить что-то в середину без переписывания всей программы?
А может просто они пытаются писать на нем то, что стоило бы писать на чем-то другом, более подходящем? Ну, знаете, вместо лома подметать веником. Но вместо этого вы пытаетесь заменить лом сначала связкой ломов, потом вилами, потом граблями, добавляете им зубцов, делаете из мягкого железа и в виде лент. Да, в итоге получился металлический веник, подметать удобнее, хотя дорогой паркет царапает, но теперь
косплеить Фрименаиспользовать его как лом уже нельзя.Само по себе добавление JIT-а или использование библиотек, написанных на другом языке, не делает ваш язык "плохим". Плохим он становится, когда вы начинаете оглядываться, когда вы вынуждены оглядываться на все эти джиты и подобное. Утрированно, вот раньше вы могли расположить переменные в любом порядке, а теперь джиту нравится, только когда порядок вот такой и никакой другой. Причем еще и скачет непредсказуемо, если вы вдруг добавляете переменных, потому что там какой-нибудь хеш считается от списка переменных. И вот вместо того, чтобы думать над задачей, вы начинаете пляски, как вам расположить переменные таким образом, чтобы JIT или сборщик мусора или еще что-нибудь не сломалось. Вы можете сколько угодно знать и оправдывать такое положение вещей, с пеной у рта доказывать, что иначе сделать нельзя, так как абстракции текут, и вообще, программисты совсем обленились, должны же они хоть что-то делать, но это именно то, что делает язык "плохим", хоть ты тресни. И всего этого нет и не будет в книгах "для чайников".
А мне казалось, что наиболее тупые языки (вроде ассемблера) как раз и являются самыми производительными. Ну или я не понял, что именно вы вкладываете с слово "тупой".
Знаете, где-то слышал:
Если у вас слишком часто текут абстракции, то может проблема в языке с этими абстракциями? Иногда можно заменить протекшие абстракции менее текущими, иногда их вообще можно выкинуть.
Как и всякое категоричное утверждение, это нуждается в доказательстве.
KanuTaH
Ну да, по-моему, в этих рассуждениях есть рациональное зерно. Чем больше производительности ты пытаешься выжать, тем ниже по технологическому стеку ты вынужден спускаться, и зачастую лучше просто использовать более подходящий язык, чем пытаться исправить дело с помощью компиляторозависимых трюков на менее подходящем. С другой стороны, эти статьи — это скорее познавательные курьёзы для демонстрации этих самых трюков, не нужно рассматривать их столь уж серьёзно.
khim
К сожалению в современных системы вы зачастую вы не можете «опуститься ниже по стеку». Даже если вы будете программировать в машинных кодах (а на большинстве современных систем ни на чём ниже вам программировать не дадут) — разница между jump и cmov никуда не денется.
Потому единственный способ в современных условиях сделать язык «хорошим» — это сделать его медленным и жрущим ресурсы. Зато всегда одинаково. Без «протекающих абстракций».
Это — действительно то, что вы рекомендуете делать?
KanuTaH
Да, но ты (утрированно) можешь сам вписать этот cmov через какую-нибудь asm директиву, а можешь пытаться шаманить над порядком переменных и операторов, добиваясь того, чтобы конкретный компилятор в конкретных условиях сделал то, что ты хочешь (а следующая версия того же компилятора все твоё сиюминутно достигнутое великолепие поломала). И в том, и в другом случае ты все равно будешь как минимум одним глазом глядеть в asm output, то есть фактически писать на ассемблере, только в первом случае — напрямую, а во втором — через… эхм… телемедицину.
P.S. Всякие SIMD интринсики у компиляторов не просто так наружу торчат, и в том же benchmarks game программы на совершенно разных языках совершенно не стесняются ими пользоваться «по месту».
0xd34df00d
В дополнение к вашему.
Я сейчас ваяю поддержку инлайн-ассемблера для хаскеля, и при написании асмокода для тестов смотрю в то, что генерирует компилятор плюсов для интринсиков в случае вроде такого: https://gcc.godbolt.org/z/WzxXr9, в связи с чем у меня два вопроса:
mov %eax, %edx
внутри цикла лишний. Как сделать, чтобы компилятор вынес его вне цикла?for
с известным числом итераций и даже с#pragma unroll
(или как там её) в массив генерирует менее оптимальный код.Короче, единственный язык, на котором можно писать максимально производительный код — это язык ассемблера, а не С и не хаскель.
А вот генерировать язык ассемблера проще не из С.
KanuTaH
В C поддержка inline assembly просто уже есть, хехе. По крайней мере, в gcc и clang :) Но вообще-то речь не столько именно об ассемблере, а, например, и о том, поймёт ли некий высокоуровневый компилятор, что некий объект в данном случае можно разместить на стеке, а не отдавать его под управление GC. Если в этом есть сомнения, а вопрос важный — лучше написать код на чем-то другом, где это отдаётся на откуп программисту.
0xd34df00d
И асмовставки являются first-class citizens? Я могу написать функцию типа unroll/unrolls выше?
Этот же аргумент означает, что если есть вообще какие бы то ни было сомнения (про девиртуализацию, или про инлайнинг горы шаблонов, или про
min
со списком инициализации, например), то лучше писать код на чем-то другом, где это отдается на откуп программисту. И сомнения-то ведь эти подтверждаются практикой!Короче, зачем писать на плюсах?
KanuTaH
Ну вот поэтому иногда и приходится писать "на плюсах как на C", как в уже набившем оскомину примере с std::min и initializer_list. Никто же не отрицает, что такое бывает. Если вопрос важный, а место горячее, лучше уж гвоздями прибить.
khim
0xd34df00d
Так для этого любой язык с FFI в C годится. Конкретный пример: было у меня предположение, что одна штука хорошо решается рагелом, ну я написал транслятор с предметного языка на рагел, плюс десяток строчек С++-клея для маршаллинга того, что сгенерённый рагелом код выплюнул, в мой код на угадайте-каком-языке. Плюсы там были нужны, чтобы использовать
std::vector
и не париться выделением-освобождением памяти руками, не более. Правда, оказалось, что рагела не хватает (ну или я там как минимум нужный мне контроль жадности не осилил, на некоторых входах он взрывался при конвертации NFA в DFA, да и не совсем регулярный язык это был, но даже на регулярном подмножестве всё было не очень), поэтому в итоге всё это выкинул и закодил самодельный велосипед с полунаркоманским недоjit'ом, но не суть.Тем не менее, зачем мне тут всё писать на С++? Зачем мне писать на С++ логику трансляции, оптимизатор предметного языка, статический анализатор предметного языка, обработчик результатов от сгенерённой стейтмашины, и так далее? Зачем мне писать на C++ работу с сетью, обвязки для принятой в компании шины сообщений (которые, к слову, на C++ были геморнее и сложнее, чем на хаскеле, несмотря на то, что плюсы — официальный язык, поддерживаемый чуваками, некоторые из которых есть в Комитете, а хаскель поддерживается полутора хасктивистами и ещё сотней сочувствующих)?
А если начинать применять то, чем плюсы действительно
хорошимогут по сравнению с большинством остальных мейнстримных языков (компилтайм-метапрограммирование всякое), то до поры до времени это всё, конечно, весело и забавно (ну там, производную в компилтайме взять, или зазеркалить иерархию functor/applicative/monad в хане), но вскоре вы упираетесь во времена компиляции, в сообщения об ошибках, в ограничения метаязыка, и в то, что авторы «остального кода» не хотят в это всё вникать.Как, опять же, конкретный пример: на одной из прошлых работ было что-то вроде фреймворка для построения пайплайнов и написания блоков этих пайплайнов — ну как gstreamer, только для всякой лоу-летенси-финансовой ерунды. Да, там шаблоны везде, CRTP, ООП-наследование-полиморфизм-статически-виртуальные-функции в компилтайме через него, короче, всё как вы ожидаете от C++-профи, всё инлайнится и шустро работает, всё супер. Только вот
Я что-то всё более скептически отношусь к тому, что на C++ можно нанимать достаточно программистов, которые могут не сломать код, и не потратят уйму времени на написание кода, которому более-менее как-то можно доверять.
khim
Это в том случае если вам нужно, чтобы продукт был надёжен и всегда работал. Но нынче это мало кому нужно.
0xd34df00d
Ну вы прям выбрали альтернативы!
Впрочем, отдел датасотонистов на другой прошлой работе писал сплошь на плюсах, когда я пришёл туда примерно в 2014-м, но как-то постепенно все стали писать на питоне, и та команда, в которой был я, переходила на питон одной из последних в 2019-м, когда я оттуда уже увольнялся. И ничего, никто не умер, свеженабранные выпускники вузов на этом самом питоне могут смело, гм, творчески перерабатывать нейросетки из публикаций под tensorflow.
Ну да, никто больше не сидит и не пишет самостоятельно какой-нибудь random forest на плюсах ковыряя байтики, чтобы всё в ОЗУ влезало и обучалось за единицы минут. Теперь апачи там везде со спарками бегают, бигдата, все дела. Но, опять же, никто не умер.
А скорость тоже мало кому нужна примерно в таком же смысле.
khim
Ну не скажите. В том же tensorflow даже раскладку по испонительным блокам CPU прикидывают… разумеется не те, кто «творчески перерабатывают сетки», а те, кто пишут ядро этого самого tensorflow…
0xd34df00d
К счастью, мне никто не ставил условий «чтоб язык реализации был в топ N».
Именно. Но это надо именно тем, кто пишет ядро TF, потому что TF — это инструмент, и достаточно хороший, чтобы можно было им пользоваться вместо написания своих велосипедов на плюсах (или на питоне, или на матлабе).
И таких инструментов становится всё больше, а потребности в умении (или возможности) писать жутко оптимизированный вычислительный код на местах — всё меньше. Поэтому в среднем потребности в условном С или плюсах (или высокопроизводительном хаскеле, в конце концов) тоже всё меньше, даже если рассматривать только то подмножество задач, где важна высокая производительность получающейся системы — просто появляются библиотеки, которые эту производительность обеспечивают, а для решения задач остаётся написать немного клея.
KanuTaH
Из воздуха появляются, наверное :) Самозарождаются, как мыши в грязном белье у ван Гельмонта, никто их разработкой не занимается, все только «пишут немного клея на питончике», и все работает :)
P.S. Сейчас ради интереса посмотрел — у того же TF почти 2.5K одних только прямых контрибуторов.
0xd34df00d
Это не отменяет того, что раньше для эффективного решения некоторого спектра задач знание плюсов было обязательно, а сейчас — нет.
khim
Но да, есть редкие исключения — там и Haskell можно и Scheme и даже Forth… но это редкость.
Тут есть некоторое противоречие — если «таких инструментов становится всё больше», то, очевидно, для их разработки требуется всё больше людей.
Пользователей этого всего — да, ещё больше… но это уже другая история.
0xd34df00d
Неочевидно, это зависит от вашей модели.
Но моделировать это — дело неблагодарное, поэтому я всё ж продолжу руководствоваться тем, что практика — критерий истины. А на практике субъективно как минимум в одной из типа интересных мне областей спрос на плюсистов сильно падает.
khim
Где-то и когда-то я это уже слышал. В школе ещё. Кажется Prolog должен был убить C++… или Lisp? Уже не помню. Помню что не срослось… Но Java точно должна была «закрыть тему». А ещё и C#, Python, PHP и ещё пара десятков других языков.
Собственно отсюда и рождается ограничение на языки. Про то, что специалисты по Cobol, Fortran, C++ или даже Java лет через 20-30 будут в наличии — очевидно.
А вот со всякими модными, но молодыми языками — вопрос очень сложный…
0xd34df00d
Знаете эту историю про гуся, который думал, что его не зарубят никогда, потому что его не зарубили вчера и позавчера?
khim
Знаю, да. Жил этот гусь у нас тут в лесопарке и его таки кормили регулярно и дожил он до глубокой старости…
Ой, вы не про этого гуся? А про того, которого на ферме не так далеко от того лесопарка откармливали? Ну так это другое дело!
</sarcasm>
Чтобы ваша аналогия с гусями чего-то стоила — нужно знать кто, где и как собирается «удушить» С++.
Вот в случае с взлётом и падением Pascal я могу всё описать легко. Turbo Pascal 1.0, в 1983м — это был офигительный прорыв, по сравнению с популярным тогда Microsoft Pascal — это был прорыв. Microsoft пытался с этим бороться (QucikBasic и QuickPascal, да) — но не смог.
Потому было принято решение Microsoft Pascal закорыть и кинуть все силы на Visual Studio. И это сработало: Delphi был популярен почти только на территории СНГ, а после того, как удалось сманить к себе ещё и разработчиков Borland C++… вопрос был закрыт.
Ахилессовой пятой Pascal оказалось то, что, фактически, популярность получил не Pascal как таковой, а один, конкретный (причём нестандартый) диалект…
А вот где вы видите кого-то, кто так же вот поступит с C++ (и, главное, почему это ему позволят сделать) — мне неясно.
0xd34df00d
Да, вы-то про COBOL вспомнили. Ну да ладно, мне тоже хватит сарказмировать.
Заметьте, что вы обсуждаете несколько другой вопрос: умрут ли плюсы или не умрут. Не умрут, конечно, я с вами даже спорить не буду. Кобол вон тоже не умер.
Но, по крайней мере, лично мне важно не это. Важно то, насколько оправдан технологически (а не по велению эйчаров
на галерах) выбор плюсов для новых проектов.Ну и ещё лично мне важны некоторые субъективные эмоции при работе с кодом на плюсах. И в последнее время их всё меньше позитивных и всё больше негативных,
Но, конечно, плюсы — всё ещё мой самый любимый императивный язык. Наверное, это синдром утёнка (хотя моим первым языком был не C++) пополам со стокгольмским синдромом и loss aversion. Годы, потраченные в том или ином виде на задрачивание особенностей языка, иногда, как говорится, the hard way, жалко спускать в унитаз, а они будут спущены в унитаз, потому что этот опыт очень плохо трансферится.
Antervis
«вот был у нас проект на языке X, мы допустили типовую для этого языка ошибку Y и столкнулись с типовой проблемой Z, а вот если бы мы писали на языке A, то с проблемой Z мы бы точно не столкнулись (и давайте промолчим про проблему B)».
Пусть X = java, Z = потребление оперативки. Или X = python, Z = низкое быстродействие. Или X = rust, Z = отсутствие наследования. Или X = haskel, Z = отсутствие человека, способного выразить логику программы в функциональном стиле. При этом для таких проблем обычно существуют типовые решения. Например, в расте вместо ООП советуют использовать ECS. А в доброй половине комбинаций проблема/яп типовым решением будет переписать на плюсы.
И тут возникает резонный вопрос: почему для других языков нормально иметь типовые проблемы и типовые же методы их решения/предотвращения, а плюсам — нет?
0xd34df00d
Не могу назвать отсутствие наследования само по себе типовой проблемой.
Но не суть. Какая типовая проблема была в данном случае у плюсов? Использование метапрограммирования? Как обойти эту проблему иначе в рамках данной задачи, оставаясь на плюсах или на сях?
Antervis
Примерно то же самое мы наблюдаем в плюсах когда человек пытается архитектуру приложения сделать полностью шаблонной, там, где отлично бы справились старые добрые виртуальные классы (интерфейс + реализация).
0xd34df00d
Ну вот в предыдущем вашем примере человек пытается свои привычные паттерны из более ООП-языков переложить на раст, и у него это не получается (так что, кстати, это не проблема раста). А какие привычные паттерны из каких других языков перекладываются тут?
Нет, не справились бы. Диспатчинг в рантайме — слишком медленно, когда вам важны наносекунды.
Antervis
LTO и девиртуализация тоже будут работать. А если уже и их будет не хватать, то там, где именно, можно и на шаблонах. А если вы боретесь за наносекунды в масштабах всего проекта, то у вас не такой уж и большой выбор языков программирования чтобы привередничать.
0xd34df00d
Разве шаблоны и вот это всё не преподносятся в том числе как средство для эффективного решения подобных задач, требующих программирования во время компиляции?
Девиртуализация там работала не очень хорошо по ряду причин, да и в любом случае, как там KanuTaH выше написал? «поймёт ли некий высокоуровневый компилятор, что некий объект в данном случае можно разместить на стеке, а не отдавать его под управление GC. Если в этом есть сомнения, а вопрос важный — лучше написать код на чем-то другом, где это отдаётся на откуп программисту.»?
Это ведь не только к GC относится, да?
Какая разница, чем мне генерировать код на условном С (или LLVM IR, как я делал на другом проекте)?
Antervis
0xd34df00d
Эм, ну да. Но если нет других способов добиться той же производительности без кодогенерации, то является ли это проблемой?
Не всегда. Если у вас весь пайплайн перекладывает байтики из входа драйвера сетевой карты в выход драйвера сетевой карты, то он весь горячий. И если вы пишете блок, как-то эти байтики преобразующий, то этот блок должен знать про своих соседей, чтобы аккуратно класть им куда надо результаты своих преобразований, чтобы избежать виртуальных вызовов и прочих индирекшонов.
AnthonyMikh
А вы можете привести пример, когда использование наследования прям-таки необходимо? Потому что мне на ум приходит только GUI.
AnthonyMikh
Вот про это я бы почитал. Не планируете писать статью?
0xd34df00d
Вряд ли получится. То было года три назад, доступа к тем исходникам я больше не имею (так как ушёл с той работы), да и если бы имел, то не уверен, что имел бы право рассказывать с достаточной степенью детализации.
technic93 Автор
Анроллы можно делать в С++, я не знаю где они есть в таком же виде ещё. Теперь у вас тоже есть, это здорово. Интринзики это все же уровень выше чем асм и их часто хватает.
А по-поводу
mov
, дает ли это измеримую разницу?0xd34df00d
То есть, вы можете написать функцию, которая примет значения
i
от1
до8
и шаблон вставки вродеи сгенерирует последовательность
которая может использоваться внутри асмовставки?
Можно пример?
Я ещё не бенчмаркал. Но с точки зрения контроля это неважно.
technic93 Автор
А так нет, я думал вы про интринзик функции как в примере на си.
khim
Всё зависит, как мы видим, от того, кто куда идёт. И зачем.
Я предлагаю? Нет. Я просто констатирую факт — вы можете этим заниматься. Или не заниматься. Разница будет примерно раз в 5-10. Как и в случае с простым шагом (метр-полтора в секунду или около того) и забегом рекордсмена (10 метров секунду или около того).
Серьёзно? Вот смотрю я на Fortran II 58го года — и чёт не вижу я таких требований. Потому смысл дальнейшей тирады, извините, от меня успользает.
Обязательная нумерация строк в Basic для мини-компьютеров появилась для удобства — и действительно, если у вас нет приличного экранного редактора — это удобнее, чем попытка рассчитать на какой строке у вас там что находится.
Когда текстовый редактор совместили с компилятором (Turbo Pascal 1.0), то стало понятно, что «есть решение получше».
А вот в случае с выбором между jump и cmov… нельзя сказать что будет лучше, если вы не знаете с какими данными собрались работать.
Вы идиот? Или играете оного на TV? JIT — это протекающая абстракция. Библиотека, написанная на другом, гораздо более быстром языке — это тоже протекающая абстракция. Да даже банальный кеш процессора — это ещё одна протекающая абстракция.
Как только вы вносите в вашу систему протекающую абстракцию — вы тут же теряете ваше свойство «хорошести»: Извините.
Это вам только так казалось. Программа 20-летней (или, ещё лучше, 50-летней) давности на ассемблере будет работать сильно медленнее, чем программа той же давности, написанная на C или Fortran. Потому что ассмеблер — он ни разу не исключение, каким вы хотите его представить. Он тоже развивается — и тоже примерно так же, как все остальные языки.
Да всё вы понимаете. «Тупой» для ассмеблера — это без всяких movnti/prefetch2 и movaps/movdqu. И он таки медленее, чем более сложный ассемблер где все эти штуки имеются.
Всё может быть. Но вот мы тут с вами переписываемся на сайте с названием Хабр и вы тут толкаете речи в защиту «хорошего языка» под названием ассмеблер. Ну и где ваш хороший браузер, написанный на этом хорошем языке? Вы же им пользуетесь, правда?
Так что… или ссылку — или, извините, вы таки Гретый Туборг.
Mingun
А живет он, наверное на стадионе и межкомнатные двери у него шириной с ворота самолетного ангара — с таким шагом нужно много места. Давайте не будем смешивать то самое оглядывание на JIT, попытку доказать, чей язык круче и решение повседневных задач.
Пока что в вашем примере я вижу одного тюнингованного человека для выполнения работки раз в… 1-3 года? Болид формулы 1 тоже один заезд ездит. Потом его выкидывают. Вы же, я надеюсь, пишите свои приложения несколько на иных принципах.
Но 100 метрах. На 1 километре разница тоже будет такая же, только участники местами поменяются. Все как я и говорил в самом начале — заточились на входные данные, на особенности железа, добавили мелких хаков, хотя задача была дойти из точки А в точку Б.
А дальше в Basic увидели. Или вы зарей считаете только самый-самый первый язык программирования? А что тогда так поздно — википедия сообщает, что можно еще до 19 века начинать отсчет. На английской первый год другой, если вдруг русской не доверяете, но тоже до 1900 года.
И правильно — расчетом должен заниматься компьютер. Для этого в более "хороших" языках придумали метки.
А я где-то утверждал обратное? Другое дело, что вы должны быть способны предсказать, что если вы используете эту конструкцию, то будет
jump
, а если эту — тоcmov
, а при использовании третьей вы сознательно отдаете решение на откуп компилятору. При этом, если от незначительного изменения входной программы результат меняется сильно (по аналогии с вычислением хеша), и это никак не отмечено явно, то такой язык "плохой". Вместо того, чтобы думать над алгоритмом вы вынуждены думать о побочных эффектах.Вот на примере данный статьи у одного человека замена
min({a, b, c})
наmin(a, min(b, c))
приводит к сильным изменениям, а у другого нет, на одном компиляторе приводит, а на другом нет. Т.е. имеем вычислительную нестабильность не только от смены представления одного и того же алгоритма, но даже от версии компилятора/реализации стандартной библиотеки!Я никаких свойств хорошести не приводил. Я их не знаю. У нас не бинарная логика "хороший — плохой". Я указал лишь факторы, делающие язык "плохим". Опять же, не в абсолютном смысле, а только по сравнению с другими языками (но это, надеюсь, и так понятно).
Да неужели? Программы на С или фортране, наверное, имеют самомодифицирующийся код и при запуске на новых процессорах сами себя перепишут под новые инструкции, а на тупом ассемблере не догадались так написать. Гм.
Вопросы же переносимости давайте оставим за кадром, мы не о них говорим
Если вы все же перечитаете мой комментарий, то вряд ли найдете там защиту хоть каких-то языков, а тем более определение каких-то языков в "хорошие".
Ассемблер — это просто язык для записи опкодов. Добавление опкодов не делает сложнее язык, лишь процессор, их выполняющий. Усложнением языка будет добавление в него макросов.
Аналогией вашему высказыванию будет, что Си, поддерживающий в идентификаторах Юникод, сложнее, как язык, чем Си, их не поддерживающий.
И, наконец, последнее.
Именно что Хабр, а не базар. Поэтому у вашего комментарий, на который я отвечаю, минус от меня. Переходить на личности с оскорблениями… плохо
khim
Нет, программы на C всего лишь могу изменить своё поведение (и существенно так изменить), если их перекомпилировать. Программы на ассемблере — нет.
Но я боюсь если вам даже это нужно разжёвывать, то остальная дискуссия большого смысла не имеет.
Возможно. Но изображать из себя дебильную девочку, не умеющую в логику — ещё хуже. Да, я знаю: для того, чтобы заниматься политикой нужно уметь в двоемыслие. Но вот ракету там или ядреный реактор так уже не сделать — физика в двоемыслие не умеет.
А Хабр — это скорее о ракетах, чем о том, как обвести толпу вокруг пальца и получит за это денег в свой карман…
Mingun
Это из чего вы такой вывод сделали?! В небинарной логике
хорошо != !плохо
, из чего не следует, чтохорошо = плохо
. Хотя, вот как ни странно, некоторые качества могут быть одновременно хорошими и плохими. Возьмем шаблоны C++. Они Тьюринг-полны — это круто, можно выразить что угодно. Они Тьюринг-полны — это кошмар, как их отлаживать? На сегодняшний день даже интерпретатора шаблонов нет! Мало того, код шаблонов обычно никто не комментирует, что хотя бы изредка делают с обычным кодом. Вопрос даже шире — их в общем случае невозможно скомпилировать из-за проблемы останова.Кто ж с этим спорит. Для этого в C и вводились абстракции. Но это как раз вопрос переносимости на другие процессоры и архитектуры. И не всегда эти самые существенные изменения будут в лучшую сторону, кстати. А если вы будите компилировать новыми компиляторами под старые процессоры, то вы хоть извернитесь, новых команд в выхлопе не получите. Странно, что вам этого не понятно. Поэтому возвращаю вам ваше замечание про логику.
AnthonyMikh
Да, но новый компилятор может лучше оптимизировать код. Например, заменить деление на 3 на сдвиг и умножение.
0xd34df00d
Именно.
Моя цель — показать, какие изменения как влияют на хаскель-код. C или плюсы меня интересуют исключительно как некоторый бейзлайн, на которых относительно просто реализовать относительно оптимальный код (по крайней мере, для задач данного объёма). Если при этом оказывается, что реализация на хаскеле с тем же алгоритмом оказывается быстрее реализации на С с тем же алгоритмом — что ж, я не стесняюсь и пишу про это.
Правда, оказывается, что кого-то это задевает, да настолько, что начинаются (совершенно простительные для бенчмарков в пользу С, конечно) вещи из последнего написанного мной абзаца. Да и вообще, посмотрите на этот тред — кому-то так не понравилось, что я пишу, что человек решил пройтись почти по всем моим комментам, включая чисто фактологические или упоминания результатов экспериментов.
Короче, не понимаю, почему вы эту часть комментария адресуете мне, а не авторам статей, где происходит вышеупомянутый цирк.
technic93 Автор
Я же делаю тоже самое, показал как микро изменения влияют на код на С++. Плюс почему и как это зависит от данных.