Comments 142
Поэтому:
Бейсик, думаете сможет сделать что-то лучше?
void main() {
write(1, "Hello World!\n", 13);
}
В этом случае мы сразу сразу пропускаем пункты с 0 по 6 из заключения статьи.
P.S.
Правда в этом случае линтер ругаться будет если мы не заинклудим unistd.h
Но в этом случае достаточно просто определить этот write.
В итоге это:
int write(int fildes, const void *buf, int nbytes);
void main() {
write(1, "Hello World!\n", 13);
}
И компилится и работает без каких либо проблем.
int main;
Просто забавный оффтоп
напрямую дернуть системный вызов write()
на самом деле вы вызвали glibc-обёртку для системного вызова.
DO
..
LOOP WHILE TRUE
или
DO WHILE TRUE
..
LOOP
Dmitri-D
Ну, если так же развернуть для какого-нибудь Quick Basic, или старого MSCC под DOS, то там будет простое ah=9h, int21h. В рамках «посмотреть, как под капотом работает HW для новичка» — да, это нагляднее, чем десять оберток над обертками. Не лучше или хуже; нагляднее. Но это так, отвлеченный коммент в сторону — ведь эта статья как раз имеет задачу показать эту самую цепочку, а не просто внутреннее устройство.
Господи, какая ностальгия напала...
Зачем табуляция перед 30?
Зачем 50 END?
Зачем заставлять писать на бэйские человека для которого это не первый язык программирования?
Так что не вижу никаких излишеств.
Ну, конкретно для отладочного printf в embedded путь может быть ещё длиннее)
Но да я знаю что он за собой тянет, тем не менее писать свой вариант sprintf еще более накладно. А нужно для того же вебсервера регулярно (и не только его, запись в файлы, общение с gsm-модулями и прочим, прочим). Да, можно и иначе сделать, но тогда придется что-то заметно урезать. Пока хватает ресурсов — применение sprintf весьма оправданно. ИМХО конечно. Например в высоконагруженном месте печать float сделал по собственной схеме, что позволило существенно ускорить процесс.
И мы ещё даже не дошли до процессов, которые при этом происходят в железе.
… не через gcc, разумеется.
call _printf
Однако замена в линуховом gcc просто знаковая. Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.
Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.
Исполняется именно то, что написано. Это основное требование к компиляторам. Просто в данном конкретном случае компилятор знает, что puts делает ровно то же самое, что и printf, но гораздо быстрее.
Нет. В качестве примера первой «самой простой» программы Керниган взял программу, которая выводит одну строку и завершается. Именно это написано в тексте программы и именно это делает программа после компиляции и выполнения. И делает это вполне просто и однозначно с точки зрения программиста, пишущего Hello World.
А то, каким образом она это делает под капотом — к Си как к языку никоим образом не относится. Это детали реализации компилятора, стандартной библиотеки и операционной системы.
А если так сильно хочется попричитать по поводу "слишком сложного и неоднозначного" hello world, то вы идите дальше — жалуйтесь на то, как неоднозначно выполняется код на разных архитектурах процессоров. А какие сложные квантовые эффекты в чипе процессора этот якобы "простой" код вызывает! Жуть! Ужасный пример Керниган взял, слишком уж всё сложно и неоднозначно.
Но вообще, в последнем издании Страуструпа, где он учит программированию на примере С++ (http://www.stroustrup.com/Programming/), он явно не ставит себе цели сразу объяснить, как оно все работает внутри — а наоборот, призывает использовать удобные абстракции, так что cout здесь вполне себе адекватно выглядит.
И если я правильно помню, то стрим запоминает все модификаторы и потом в явном виде нужно откатить все обратно?
Это правда, но на тот момент (я не умел программировать вообще), возможность не разбираться (и не ошибаться) в форматной строчке для меня была намного важнее — std::cout и std::cin казались намного более понятными в использовании, чем printf/scanf.
Есть же какое-то .)
Так я ж и написал про длинную колбасу из модификаторов стримов: ширину задай, заполняющий символ задай, основание системы счисления задай. А потом верни все обратно — т.е. снова задай, но уже значения по умолчанию.
Конкретные решения в духе «setw сбрасывается при каждой форматной операции, а остальные настройки — нет» могут быть странными, да. Но printf никто не запрещал, если кому лениво (мне обычно да — даже в глубоко C++ коде предпочитаю C-style I/O, если нет явных причин так не делать).
Если кому нужен эффективный printf-like, то для него есть Boost.Format — тот разбирает форматную спецификацию при компиляции. Заодно там ещё вкусностей (типа эффективное формирование строки, чтобы можно было, например, подробный exception сделать без промежуточного ostringstream).
1980: C
printf("%10.2f", x);
1988: C++
cout << setw(10) << setprecision(2) << fixed << x;
1996: Java
java.text.NumberFormat formatter = java.text.NumberFormat.getNumberInstance();
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(2);
String s = formatter.format(x);
for (int i = s.length(); i < 10; i++) System.out.print(' ');
System.out.print(s);
2004: Java
System.out.printf("%10.2f", x);
2008: Scala and Groovy
printf("%10.2f", x)
(Thanks to Will Iverson for the update. He writes: “Note the lack of semi-colon. Improvement!”)
2012: Scala 2.10
println(f"$x%10.2f")
(Thanks to Dominik Gruntz for the update, and to Paul Phillips for pointing out that this is the first version that is checked at compile time. Now that's progress.)
Еще раз. Тот факт, что вызов printf в данном случае транслируется в вызов puts вызван не языком С, а компилятором gcc (конкретной версии, использованной автором) и glibc версии 2.17. Когда Ричи с Керниганом работали над Си и хеллоуворлдом для него — ни gcc, ни glibc еще и в помине не было.
Кому должен? Поведение puts и printf в данном случае одинаково. printf при этом более универсален в общем случае.
Чем конкретно он хуже puts-а в контексте хеллоуворлда и си как языка?
Чем конкретно он (она — printf()) хуже puts-а в контексте хеллоуворлда и си как языка?
1. тем, что это одна из самых сложных и неоднозначных функций Си (для первого примера — худший выбор)
2. тем, что она здесь не нужна (для любого примера — странный выбор)
3. тем, что она тупо зря гоняет процессор/препроцессор в поисках подстановок %i, которых там нет (для отдельного примера неважно, но для постоянного использования — плохой выбор.)
3. тем, что она тупо зря гоняет процессор/препроцессор в поисках подстановок %i, которых там нет
Так она и не гоняет — компилятор направляет по пути наименьшего сопротивления — подменяет на puts.
А вот если бы в форматной строке были переменные — таки да, погнало бы по большому кругу и ассемблерный код был бы значительно сложнее.
Просто одна из моделей оптимизации компилируемого кода.
В контексте хеллоуворлда printf выступает выступает в качестве стандартного способа вывода текста в стандартный поток вывода. В реальном мире она этим и является — стандартным способом вывода текста, используемым по-умолчанию, если нет каких-то особых требований.
Какие преимущества использования узкоспециализированного puts? Процессор впустую подстановки не ищет? Так он и так не ищет, современный компилятор вон оптимизирует такое, как показано в статье. Универсальная printf "не нужна", а специализированная puts нужна? Так это не так работает. В 90% случаев используют общепринятый инструмент прежде всего, а на специализированные переходят если на то есть причины. И "вот конкретно эту вещь специализированный тоже может сделать" — это не причина.
одна из самых сложных и неоднозначных функций Си
При форматированном выводе — да, может быть сложно вспомнить/разобраться во всех этих спецификаторах форматов и типов. Но при выводе строки она не сложнее puts. А по однозначности даже получше будет — не дописывает "самовольно" переводы строк в поток.
Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки? Или их такое переопределение считается как UB?
Или их такое переопределение считается как UB?Их поведение описано в стандарте. Если вы его меняете, то получаете среду несовместимую со стандартом — и тут уже говорить о том, UB это или не UB смысла не имеет.
Из того, что вы при компиляции не давали флаг -ffreestanding.
Не давали => сборка под hosted => libc со стандартными свойствами, в которых printf и puts взаимозаменяемы описанным образом (а fprintf и fputs — чуть иначе, но тоже).
Там, где это не так, можно собирать для freestanding (так делают, например, во FreeBSD для ядра, rtld и ещё немного специфических компонентов).
Есть спецификация языка. В случае Си — это стандарт ISO/IEC 9899. Все, что не входит в эту спецификацию частью языка не является. Компилятор (и уж тем более его конкретная реализация) туда не входит. Стандартная библиотека входит, но её реализация — не входит.
Так все и было. Сначала был один компилятор. Потом их стало три. Каждый со своими правилами. И тогда вместо одного языка стало три. Умные дяди почесали затылки и выкатили стандарт на язык. Именно на язык, а не на конкретный компилятор.
И сказали так: О, Компиляторы! Вы цари и боги, и вольны делать все что хотите, покуда не нарушаете стандарт, иначе несоответствующими стандарту заклеймлены будете.
Таким образом, стандарт — это, конечно, просто бумажка. Но компилятор должен этой бумажке соответствовать. Для того она и существует.
Вы мне пишите, что стандарт нужен, чтобы «компилятор соответствовал языку», то есть сохранялась целостность языка и компилятора, то есть чтобы они были «единым целым». Зачем? Это ведь моё изначальное утверждение.
И это не будет нарушением стандарта.
Собственно, я думаю, причина всех этих минусов в том, что вы в своём посте включили компилятор в язык.
Язык он сферический в вакууме. Стандарт создан, конечно, с расчетом на то, как именно язык будут использовать, но записан он очень оторвано от реализации.
Завтра у нас появится транслятор, скармливающий программы на языке в надмашинный мирровой квазигипермозг, который посмотрев на код будет без всякой компиляции сразу выдавать результат выполнения…
И это никак не повлияет на сам язык.
Язык без компилятора (интерпретатора, транслятора) недееспособен. Поэтому они — целое, что и вы вроде подтвердили, а вроде и нет.
Компиляцию в язык включил не я, а авторы первого высокоуровневого языка Фортран — фор[мула +] тран[сляция]. Но зачем держать в голове глупые факты.
Я как раз утверждал, что язык и компилятор — единое целое.Что, собственно, и является грубой ошибкой. У каждого компилятора есть куча особенностей, которые использовать нельзя.
Например первые компиляторы располагали переменные в стеке подряд и если вы писали
int a[4], b;
то вы могли обращаться к b
, как к a[4]
. Но частью это языка не было и поломались, в общем, довольно скоро. А ещё там можно было к int
у прибавлять единичку пока не получится -1 — но, опять-таки, в стандарте это запрещено и в современных компиляторах не работает.Да, компилятор, несомненно, описывает некоторый язык — но это не C! У него есть много свойств, которые в спеку не входят и полагаться на которые опасно. Потому что завтра, даже просто на другой машине, компилятор может повести себя по-другому — и вы получите кучу проблем.
А вот спека — она неизменна. Не зависит ни от машины, ни от желания левой пятки разработчика компилятора.
Изначальное управляемое видением автора языка, теперь — стандартом.В любом компиляторе, всегда, есть вещи, которые туда автор не планировал закладывать — но они там есть. Просто «потому что так получилось». Опираться на них нельзя. Вот поэтому язык — это, в первую очередь, спека, а во-вторую компилятор.
В других языках (скажем Java) прилагаются очень серьёзные усилия к тому, чтобы программист не смог «усмотреть» в компиляторе чего-то, чего нет в спеке, в C/C++ — такие усилия не прилагаются. Правильная программа, соответствующая спеке — обязана работать, неправильная — тоже может работать, но что она, при этом будет делать — разработчиков не волнует от слова совсем. И это принципиальная позиция разработчиков. Потому в Java можно сказать, почти не покривив душой, что компилятор и язык — это одно и то же, но в C/C++ — нельзя.
Но это пол беды. Логика работы с буфером находится в динамической библиотеке, доступ к которой пользовательская программа имеет через таблицу указателей на функции.
Вот и получается, что мы сначала делаем jump в динамическую либу, потом кидаем данные в буффер… И, только если левая пятка сподобится, вызываем write…
Насколько такая цепочка оправдана… Вопрос открытый.
Так что единственное доставшееся нам объяснение: «У нас так принято».
Ну, например, во FreeBSD libc всё это происходит гораздо проще:
int
puts(char const *s)
{
int retval;
size_t c;
struct __suio uio;
struct __siov iov[2];
iov[0].iov_base = (void *)s;
iov[0].iov_len = c = strlen(s);
iov[1].iov_base = "\n";
iov[1].iov_len = 1;
uio.uio_resid = c + 1;
uio.uio_iov = &iov[0];
uio.uio_iovcnt = 2;
FLOCKFILE_CANCELSAFE(stdout);
ORIENT(stdout, -1);
retval = __sfvwrite(stdout, &uio) ? EOF : '\n';
FUNLOCKFILE_CANCELSAFE();
return (retval);
}
Вызываемая далее функция длинная, аж три экрана:
/*
* Write some memory regions. Return zero on success, EOF on error.
*
* This routine is large and unsightly, but most of the ugliness due
* to the three different kinds of output buffering is handled here.
*/
int
__sfvwrite(FILE *fp, struct __suio *uio)
{
но коммент и извиняется об этом. А вызывает она в себе
int
__swrite(void *cookie, char const *buf, int n)
{
FILE *fp = cookie;
return (_write(fp->_file, buf, (size_t)n));
}
ну а _write
уже обёртка сисколла. Как видно, разобраться с буферизацией можно и без ООП-стиля таблиц указателей на функции.
Эм, что? Почему-то мне кажется, что, может быть, в 50%, но никак не в 99%.
И, собственно, чем же вы предлагаете делать форматированный вывод в реалтайме, если вдруг стало очень надо?
printf() в этом плане устойчивее, поведение всегда одинаково, а дополнительный скан на '%' стоит доли копеек по сравнению даже с переходом в ядро.
На этом микроядра погорели. Современная архитектура, вообще очень плохо «ложится» на современные стили написания программ. Что довольно грустно.
Так-то хорошо было бы жить в мире где то, что считается «хорошим стилем» было бы не только «красиво», но и «эффективно»… Но уж где живём — там и живём…
И попытки ускорить не провалились. От тысяч тактов мы перешли примерно до 300-400 тактов. А последующие улучшиния довели это время до сотни примерно.
Проблема в другом. На каком-нибудь 80286 — «поход в ядро» тоже под сотню тактов занимает… но там и вызов функции — тактов 20-30. А у современного процессора это получается сделать за 5-6.
То есть проблема не в том, что кто-то замедлил «поход в ядро». Проблема в том, что «обычные» операции стали сильно быстрее работаеть.
P.S. Кстати очень хорошо видно почему те же люди, которые устроили все эти индирекции в GLibC грязно ругают тех, кто поверх этого десять слоёв абстракции навесил — они абсолютно правы. Потому что «навесить» 3-4 «быстрых» вызова по 5-10 тактов и за счёт этого получить вдвое меньше системых вызовов — это таки очень-таки неплохая экономия. Один системный вызов как 20, а то и 30 вызовов функций стоит. А вот если вы добавляете к цепочке функций ещё один слой и не получаете уменьшения количество вызовов в каком-то месте… то это в чистом виде замедление.
До чего же было приятно читать: все разобрано шаг за шагом, компактно, с соответствующими листингами, объяснениями и т.д.
Большое спасибо за статью (и перевод)!
Ещё раз — это свойство не языка, а конкретного компилятора в конкретных условиях и с применением конкретных версий конкретных библиотек. Всей этой лапши в других условиях может просто не быть. Но может быт другая. Впрочем — в каком языке тот же функционал будет сделан проще?
А насчёт простоты Си — сейчас придет khim и вам расскажет, что он об этом думает )))
Это свойство конкретной glibc в конкретном Линуксе. Во FreeBSD например всё проще и читабельней https://habr.com/ru/post/438044/#comment_23384858
Вообще я думаю, поправьте меня, что все дело в том, что функция printf исходно нужна для форматированного вывода, а не для вывода константных строк (хотя их она тоже может выводить). И ее использование в этом примере просто является плохой практикой, которая пошла в массы. И когда массы видят такую вот сложную замену на puts и далее по тексту — их выворачивает. Если бы сразу было честно написано puts(string), либо не менее честно printf("%s",string), то не было бы этих проблем. Более того, в первой книге по Си, которую я читал в универе, годов так 80-х (переводная) хеллоуворлд был вообще с putchar и циклом, видимо чтобы сразу отбить охоту писать на Си. Вот как надо делать!
Может вы что-то другое пишите? Например если написать return printf(«Hello World!\n»); — то он, разумеется, перестаёт менять…
Это гораздо интереснее, если честно :)
Но очень платформоспецифично.
Анализ сишного Hello World