Pull to refresh

Comments 142

И эти люди ругают нас за наворачивание абстракции над абстракцией.
Так тут всё вполне просто, никаких наворотов.
В том и дело, что в системе итак достаточно абстракций, хватит плодить ещё больше.
Проблема не в том, что мы порождаем абстракции, а в том, что мы порождаем абстракции для решения проблем, уже решённых существующими абстракциями! Вот это — настоящий перпетум мобиль, с которым нужно-таки бороться.
Собственно, «C» закончился вызовом библиотечной функции.
Бейсик, думаете сможет сделать что-то лучше?
Вообще, если говорить о урощении, то можно же напрямую дернуть системный вызов write()
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);
}

И компилится и работает без каких либо проблем.
Нужны бенчи и тесты :) по самому быстрому выполнению хелловорлда :)
UFO just landed and posted this here
add %al,(%rax)

Т.е. оно компилится в нули.
UFO just landed and posted this here
напрямую дернуть системный вызов write()

на самом деле вы вызвали glibc-обёртку для системного вызова.

UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Это от диалекта зависит, resume тоже бывает
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
А зачем писать GOTO 10, если можно написать

DO
..
LOOP WHILE TRUE


или
DO WHILE TRUE
..
LOOP


Dmitri-D
Ну, если так же развернуть для какого-нибудь Quick Basic, или старого MSCC под DOS, то там будет простое ah=9h, int21h. В рамках «посмотреть, как под капотом работает HW для новичка» — да, это нагляднее, чем десять оберток над обертками. Не лучше или хуже; нагляднее. Но это так, отвлеченный коммент в сторону — ведь эта статья как раз имеет задачу показать эту самую цепочку, а не просто внутреннее устройство.
UFO just landed and posted this here
В таком случае — да, но просто метка 10, как традиционно метка первой строки, не кажется подходящей для вашей интерпретации. Скорее, изначально имелся в виду бесконечный цикл в классическом 10 print… goto 10.
А для выхода из цикла можно так же текстовую метку использовать.
UFO just landed and posted this here

Господи, какая ностальгия напала...

Зачем подчекивание в строке 20?
Зачем табуляция перед 30?
Зачем 50 END?

Зачем заставлять писать на бэйские человека для которого это не первый язык программирования?
Хорошо, это был файловый дескриптор, ассоциированный с терминалом. А мог быть stdin другого приложения или вообще символьное устройство /dev/null. Т.е. другими словами printf, как и puts — это разновидность межпроцессного взаимодействия. Что если не ядро должно заниматься межпроцессным взаимодействием?
Так что не вижу никаких излишеств.
UFO just landed and posted this here

Ну, конкретно для отладочного printf в embedded путь может быть ещё длиннее)

UFO just landed and posted this here
А вот я бы не стал так утверждать, ибо регулярно использую sprintf :-)
Но да я знаю что он за собой тянет, тем не менее писать свой вариант sprintf еще более накладно. А нужно для того же вебсервера регулярно (и не только его, запись в файлы, общение с gsm-модулями и прочим, прочим). Да, можно и иначе сделать, но тогда придется что-то заметно урезать. Пока хватает ресурсов — применение sprintf весьма оправданно. ИМХО конечно. Например в высоконагруженном месте печать float сделал по собственной схеме, что позволило существенно ускорить процесс.

Я sprintf побаиваюсь, предпочитаю snprintf.

Вы абсолютно правы. Но практически у меня бывали и исключения, хотя вот вспоминаешь и думаешь — а не вернуться ли и не переделать ли :-)
Хм, надвно наблюдал терминал оплаты сбера с ошибкой malloc, хотя это тоже должен быть embedded…
UFO just landed and posted this here
Так то не Сбер, а Верифон, с его прошивками. Да и там такой себе эмбеддед вроде, это довольно мощная железка уже. Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.
Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.
Там такая себе Java, спцфцкая. Без GC и free — отличный подход к выделению памяти! Malloc там, фактически, аналог Sbrk…
За то, что весь этот путь информации хоть куда нибудь наружу надо проделывать самостоятельно? Даже если устройством показа пользователю является осциллограф на ножке GPIO.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Да-да, старый добрый эмбеддед. Сейчас как раз сижу с Code Composer Studio 9 и пытаюсь понять почему printf в консоль не гадит, хотя два дня назад вполне успешно это делал. Это я уже молчу, что нужно было специфически поставить опции на странице Debug, иначе отладчик вообще отваливается на попытке загрузить программу в процессор.
Когда то очень давно, выдрал из glibc одну функцию и таскаю её уже страшно посчитать сколько лет уже. Но точно помню, что когда выдирал — такого кошмара там точно ещё не было:)

А точно именно из glibc? На днях как раз всплывал printf, который другие (?) авторы выдернули, но не из glibc, а из ядра Linux — полагаю, glibc им тоже пришла в голову первой, но чем-то им исходник glibc не понравился (хотя, возможно, и тем, что исходник ядра у них на машине уже был).

И мы ещё даже не дошли до процессов, которые при этом происходят в железе.

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

Аналитически не решим, придётся численно. А там опять компьютеры, а внутри снова код приложения, код в ОС, процессы в транзисторах в CPU, снова гамильтонианы… :)
UFO just landed and posted this here
DJGPP это про защищённый режим и DPMI, абстракций там сравнимо будет, по идее.
В виндовом gcc замена printf на puts не происходит:

call _printf

Однако замена в линуховом gcc просто знаковая. Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.
Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.

Исполняется именно то, что написано. Это основное требование к компиляторам. Просто в данном конкретном случае компилятор знает, что puts делает ровно то же самое, что и printf, но гораздо быстрее.
Скажем так: в качестве примера первой «самой простой» программы Керниган взял самую сложную и неоднозначную функцию, которая к тому же не выполняется. Вот такой Hello world.

Нет. В качестве примера первой «самой простой» программы Керниган взял программу, которая выводит одну строку и завершается. Именно это написано в тексте программы и именно это делает программа после компиляции и выполнения. И делает это вполне просто и однозначно с точки зрения программиста, пишущего Hello World.


А то, каким образом она это делает под капотом — к Си как к языку никоим образом не относится. Это детали реализации компилятора, стандартной библиотеки и операционной системы.


А если так сильно хочется попричитать по поводу "слишком сложного и неоднозначного" hello world, то вы идите дальше — жалуйтесь на то, как неоднозначно выполняется код на разных архитектурах процессоров. А какие сложные квантовые эффекты в чипе процессора этот якобы "простой" код вызывает! Жуть! Ужасный пример Керниган взял, слишком уж всё сложно и неоднозначно.

Ну так и написал бы puts(). Но нет, хотелось козырнуть printf(), которая тут вообще, оказывается, не стояла. Ричи, кстати, должен был знать — он же компилятор точил.
Возможно в примерах часто был нужен именно форматированный вывод. Наверное поэтому и использовал printf.
Сам Керниган признаётся во вступлении, что «не копенгаген» в ассемблере, оно и видно. А Hello world зря взяли в «самые простые» программы. У Строструпа она выглядит совсем кошмарно — cout << «Hello world». Этим надо заканчивать курс СПП, а не начинать.
В свое время форматные строки для printf и scanf стали причиной, по которой я выбрал учить C++, а не С (для С взял книжку не K&R, что, конечно, было ошибкой).

Но вообще, в последнем издании Страуструпа, где он учит программированию на примере С++ (http://www.stroustrup.com/Programming/), он явно не ставит себе цели сразу объяснить, как оно все работает внутри — а наоборот, призывает использовать удобные абстракции, так что cout здесь вполне себе адекватно выглядит.
Ну, да в С++ вообще нет форматирования. Попытки заменить printf(«Value1: 0x%08X, value2: %d, value3: 0x%08X\n», val1, val2, val3); превращаются в ужасную колбасу из << и модификаторов стримов.

И если я правильно помню, то стрим запоминает все модификаторы и потом в явном виде нужно откатить все обратно?

Это правда, но на тот момент (я не умел программировать вообще), возможность не разбираться (и не ошибаться) в форматной строчке для меня была намного важнее — std::cout и std::cin казались намного более понятными в использовании, чем printf/scanf.

UFO just landed and posted this here
Есть же какое-то .)

Так я ж и написал про длинную колбасу из модификаторов стримов: ширину задай, заполняющий символ задай, основание системы счисления задай. А потом верни все обратно — т.е. снова задай, но уже значения по умолчанию.
UFO just landed and posted this here
Я надеюсь что все более-менее опытные разработчики знают что в строке форматирования должна быть константа.
UFO just landed and posted this here
Библиотека в C++ здесь создавалась из расчёта, чтобы исключить парсинг форматной строки в рантайме — этот парсинг переложен на программиста при написании ввода-вывода, громоздко, но эффективнее.
Конкретные решения в духе «setw сбрасывается при каждой форматной операции, а остальные настройки — нет» могут быть странными, да. Но printf никто не запрещал, если кому лениво (мне обычно да — даже в глубоко C++ коде предпочитаю C-style I/O, если нет явных причин так не делать).

Если кому нужен эффективный printf-like, то для него есть Boost.Format — тот разбирает форматную спецификацию при компиляции. Заодно там ещё вкусностей (типа эффективное формирование строки, чтобы можно было, например, подробный exception сделать без промежуточного ostringstream).
Ну на эту тему Хорстман обстебался в своём The March of Progress

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.)
UFO just landed and posted this here
Десять лет назад, когда я на эту штуку впервые набрёл (или примера из 20012го года ещё не было) — это было ещё смешнее.

Особенно «Note the lack of semi-colon. Improvement!»
UFO just landed and posted this here

Еще раз. Тот факт, что вызов printf в данном случае транслируется в вызов puts вызван не языком С, а компилятором gcc (конкретной версии, использованной автором) и glibc версии 2.17. Когда Ричи с Керниганом работали над Си и хеллоуворлдом для него — ни gcc, ни glibc еще и в помине не было.

Кому должен? Поведение puts и printf в данном случае одинаково. printf при этом более универсален в общем случае.


Чем конкретно он хуже puts-а в контексте хеллоуворлда и си как языка?

Чем конкретно он (она — printf()) хуже puts-а в контексте хеллоуворлда и си как языка?

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

Так она и не гоняет — компилятор направляет по пути наименьшего сопротивления — подменяет на puts.
А вот если бы в форматной строке были переменные — таки да, погнало бы по большому кругу и ассемблерный код был бы значительно сложнее.
Просто одна из моделей оптимизации компилируемого кода.
Товарищ выше намекает, что в славные 70-е компилятор не подменял. А виндовый gcc и сейчас не подменяет. Но это не суть, а на сдачу. Суть в первых двух пунктах.
Первые 2 пункта верны на все 100.
Помнится, обучение сям начиналось с puts/gets, и только через пару-тройку месяцев переходили к комбайнам printf/scanf.

Аминь, брат! Только из-за Hello world эту истину приходиться отстаивать, как Джордано Бруно.

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


Какие преимущества использования узкоспециализированного puts? Процессор впустую подстановки не ищет? Так он и так не ищет, современный компилятор вон оптимизирует такое, как показано в статье. Универсальная printf "не нужна", а специализированная puts нужна? Так это не так работает. В 90% случаев используют общепринятый инструмент прежде всего, а на специализированные переходят если на то есть причины. И "вот конкретно эту вещь специализированный тоже может сделать" — это не причина.


одна из самых сложных и неоднозначных функций Си

При форматированном выводе — да, может быть сложно вспомнить/разобраться во всех этих спецификаторах форматов и типов. Но при выводе строки она не сложнее puts. А по однозначности даже получше будет — не дописывает "самовольно" переводы строк в поток.

В своё время было очень много взломов из-за того, что люди привыкали использовать printf где ни попадя вместо puts. В конце концов получался printf вывода пользователя или неожиданно вылезал в строке %, и всё падало. Т.е. в данном случае новичков сразу учат плохому, которое потом надо будет выправлять.
Лучше выправлять некоторую небезопасность, заставляя уже наученного работника использовать безопасные версии printf, чем учить его всему форматированному выводу.
printf тратит время на парсинг форматной строки во время исполнения, puts — нет. Замена printf на puts, в данном случае — оптимизация компилятора. А любая оптимизация увеличивает время компиляции, т.е. здесь вы платите временем компиляции. Ну и плюс ко всему, если очень хочется печатать статик строку через printf лучше сделать это так: printf( "%s", «Hello world» ); Привычка всегда использовать printf с указанием форматной строки улучшит безопасность ваших программ.
Ну если вы запихнете при запуске приложения в LD_PRELOAD библиотеку, которая экспортирует puts, но не printf, то поведение при такой оптимизации поменяется. Понятно, что на практике это маловероятно, но все же.

Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки? Или их такое переопределение считается как UB?
Или их такое переопределение считается как UB?
Их поведение описано в стандарте. Если вы его меняете, то получаете среду несовместимую со стандартом — и тут уже говорить о том, UB это или не UB смысла не имеет.
> Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки?

Из того, что вы при компиляции не давали флаг -ffreestanding.
Не давали => сборка под hosted => libc со стандартными свойствами, в которых printf и puts взаимозаменяемы описанным образом (а fprintf и fputs — чуть иначе, но тоже).

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

Есть спецификация языка. В случае Си — это стандарт ISO/IEC 9899. Все, что не входит в эту спецификацию частью языка не является. Компилятор (и уж тем более его конкретная реализация) туда не входит. Стандартная библиотека входит, но её реализация — не входит.

Поменяется компилятор — поменяются правила языка. Все эти стандарты — просто бумажки. Рулит компилятор, поэтому разделять его с языком — ошибка, точнее, идеализация. То же самое с HTML — есть поддержка в браузере (интерпретаторе) — есть в языке. Нет — гуляй с пляжа.
Если чего-то есть в спеке, но нет в компиляторе — то это повод зафайлить бегу и компилятор (или, гораздо реже, спеку) исправят… а вот если чего-то было в компиляторе, а потом пропало… то это, в лучшем случае, вызовет реакцию типа «как-же всё-таки хорошо, что вот это вот всё, что вы написали — это не наша проблема».
Стандарт языка придумали не просто так.

Так все и было. Сначала был один компилятор. Потом их стало три. Каждый со своими правилами. И тогда вместо одного языка стало три. Умные дяди почесали затылки и выкатили стандарт на язык. Именно на язык, а не на конкретный компилятор.

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

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

Вы мне пишите, что стандарт нужен, чтобы «компилятор соответствовал языку», то есть сохранялась целостность языка и компилятора, то есть чтобы они были «единым целым». Зачем? Это ведь моё изначальное утверждение.
Язык си может быть обработан интерпретатором.
И это не будет нарушением стандарта.

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

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

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

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

Язык без компилятора (интерпретатора, транслятора) недееспособен. Поэтому они — целое, что и вы вроде подтвердили, а вроде и нет.

Компиляцию в язык включил не я, а авторы первого высокоуровневого языка Фортран — фор[мула +] тран[сляция]. Но зачем держать в голове глупые факты.
Я как раз утверждал, что язык и компилятор — единое целое.
Что, собственно, и является грубой ошибкой. У каждого компилятора есть куча особенностей, которые использовать нельзя.

Например первые компиляторы располагали переменные в стеке подряд и если вы писали
int a[4], b;
то вы могли обращаться к b, как к a[4]. Но частью это языка не было и поломались, в общем, довольно скоро. А ещё там можно было к intу прибавлять единичку пока не получится -1 — но, опять-таки, в стандарте это запрещено и в современных компиляторах не работает.

Да, компилятор, несомненно, описывает некоторый язык — но это не C! У него есть много свойств, которые в спеку не входят и полагаться на которые опасно. Потому что завтра, даже просто на другой машине, компилятор может повести себя по-другому — и вы получите кучу проблем.

А вот спека — она неизменна. Не зависит ни от машины, ни от желания левой пятки разработчика компилятора.

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

В других языках (скажем Java) прилагаются очень серьёзные усилия к тому, чтобы программист не смог «усмотреть» в компиляторе чего-то, чего нет в спеке, в C/C++ — такие усилия не прилагаются. Правильная программа, соответствующая спеке — обязана работать, неправильная — тоже может работать, но что она, при этом будет делать — разработчиков не волнует от слова совсем. И это принципиальная позиция разработчиков. Потому в Java можно сказать, почти не покривив душой, что компилятор и язык — это одно и то же, но в C/C++ — нельзя.
Это прекрасно. Мне бы эту статью лет десять надад.
UFO just landed and posted this here
UFO just landed and posted this here
Тут есть два момента. Во первых puts работает с буфферизацией, то есть прежде чем выдать что-то во внешний мир, он сбрасывает данные в буффер, копит их там, а потом уже выдаёт все разом. Это делается для минимизации количества системных вызовов в частности и операций в пространстве ядра — драйверах вообще.

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

Вот и получается, что мы сначала делаем jump в динамическую либу, потом кидаем данные в буффер… И, только если левая пятка сподобится, вызываем write…

Насколько такая цепочка оправдана… Вопрос открытый.
На это наверняка были веские основания, к сожалению, те люди, которые их знали, на смогут поделится с нами по веским причинам — они все эти основания забыли.
Так что единственное доставшееся нам объяснение: «У нас так принято».
Это решение родилось в процессе эволюции системы. Тут как по Энштейну. Настолько просто, насколько возможно, но ничуть не проще.
UFO just landed and posted this here
UFO just landed and posted this here
Я имею сказать по этому поводу. Эволюции надо рассматривать в долгосрочной перспективе. Неэфективные решения эволюционно нестабильны. Через миллион лет все может поменяться.
UFO just landed and posted this here
На самом деле, никто не мешает написать myprintf напрямую через write. Это будет прекрасно работать. Можно даже обойтись без glibc совсем. Но glibc предлагает решение среднее по больнице, которое подходит в 98% случаев. И да, оно зачастую избыточно, но работает как часы…

Ну, например, во 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 уже обёртка сисколла. Как видно, разобраться с буферизацией можно и без ООП-стиля таблиц указателей на функции.

а как оно было в ТурбоС? Там вроде где-то довольно недалеко от .obj уже были бинарники с записью в конкретные железные порты.
не так уж и близко, там через int 21h писалось, а 21h уже dos обрабатывал, который затем передавал управление в bios через int 10h
Был еще вариант с прямой записью в видео память по адресам 0B800h:0000h, короче не уже и не придумаешь.
ладно хоть не было раскрутки с самого запуска приложения, там больше наслоений абстракций
а если еще вспомнить про отладку и ее пляски вокруг псевдорежимов процессора — вообще застрелиться можно…
как сейчас помню бессонную неделю студенчества когда программа «модифицировала» свои ресурсные части и отладчик сходил с ума — приходилось «отлаживать» записывая все в лог
А можно ссылочки на исходные коды и объяснение того, как вы их находили?
А почему в примере printf, а не puts?! Я прекрасно понимаю, что для данной статьи это не важно, а gcc вообще вместо подобного printf поставит puts, но сам факт того, что hello world пишут с использованием printf, заставляет новичков считать, что printf — это что-то нормальное, хотя в 99% случаев уже на стадии компиляции известно, что куда подставлять надо и парсить строку в real time уж точно никакого смысла нет. Printf пора уже сделать deprecated.

Эм, что? Почему-то мне кажется, что, может быть, в 50%, но никак не в 99%.


И, собственно, чем же вы предлагаете делать форматированный вывод в реалтайме, если вдруг стало очень надо?

UFO just landed and posted this here
Например, потому, что многих выбешивает запоминать, что puts() добавляет финальный \n, а fputs() — нет. И даже не просто запоминать, а вовремя вспоминать при замене stdout на файл или наоборот.
printf() в этом плане устойчивее, поведение всегда одинаково, а дополнительный скан на '%' стоит доли копеек по сравнению даже с переходом в ядро.
Не по сравнению «даже с походом в ядро». Поход в ядро — это не дорого. Это очень дорого. Сотни тактов. За которые суперскаляр может исполнить тысячи операций.

На этом микроядра погорели. Современная архитектура, вообще очень плохо «ложится» на современные стили написания программ. Что довольно грустно.

Так-то хорошо было бы жить в мире где то, что считается «хорошим стилем» было бы не только «красиво», но и «эффективно»… Но уж где живём — там и живём…
UFO just landed and posted this here
Ну дык эта ж проблема не на ровном месте появилась. Просто переход через границу привилегий — это реально сложная задача. Нужно много чего сделать при переходе «туда» и «обратно».

И попытки ускорить не провалились. От тысяч тактов мы перешли примерно до 300-400 тактов. А последующие улучшиния довели это время до сотни примерно.

Проблема в другом. На каком-нибудь 80286 — «поход в ядро» тоже под сотню тактов занимает… но там и вызов функции — тактов 20-30. А у современного процессора это получается сделать за 5-6.

То есть проблема не в том, что кто-то замедлил «поход в ядро». Проблема в том, что «обычные» операции стали сильно быстрее работаеть.

P.S. Кстати очень хорошо видно почему те же люди, которые устроили все эти индирекции в GLibC грязно ругают тех, кто поверх этого десять слоёв абстракции навесил — они абсолютно правы. Потому что «навесить» 3-4 «быстрых» вызова по 5-10 тактов и за счёт этого получить вдвое меньше системых вызовов — это таки очень-таки неплохая экономия. Один системный вызов как 20, а то и 30 вызовов функций стоит. А вот если вы добавляете к цепочке функций ещё один слой и не получаете уменьшения количество вызовов в каком-то месте… то это в чистом виде замедление.

До чего же было приятно читать: все разобрано шаг за шагом, компактно, с соответствующими листингами, объяснениями и т.д.
Большое спасибо за статью (и перевод)!

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

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


А насчёт простоты Си — сейчас придет khim и вам расскажет, что он об этом думает )))

То чувство, когда ты понимаешь, наверное не всегда стоит знать как оно работает. Теперь при каждом printf будет переклинивать секунд на 10 воспоминаниями этого поста…
Они просто не хотели тащить за собой огромную libc. И в принципе их можно понять.

Вообще я думаю, поправьте меня, что все дело в том, что функция printf исходно нужна для форматированного вывода, а не для вывода константных строк (хотя их она тоже может выводить). И ее использование в этом примере просто является плохой практикой, которая пошла в массы. И когда массы видят такую вот сложную замену на puts и далее по тексту — их выворачивает. Если бы сразу было честно написано puts(string), либо не менее честно printf("%s",string), то не было бы этих проблем. Более того, в первой книге по Си, которую я читал в универе, годов так 80-х (переводная) хеллоуворлд был вообще с putchar и циклом, видимо чтобы сразу отбить охоту писать на Си. Вот как надо делать!

Все же надо заметить что аналог puts/printf есть в стандартной библиотеке практически любого языка. И в большинстве библиотек тоже будет навернуто несколько слоев абстракции перед вызовом syscall конкретной платформы на конкретной архитектуре. А уж что там ядро потом делает — от языка вообще никак не зависит.
Забавно, что GCC меняет на puts даже в режиме -O0, а C++ компилятор той же версии не меняет даже при -O3. Наверно где-то в глубинах стандартов можно найти ответ, но лень…
У меня меняет. И даже G++ 4.1 (самый древний, какой есть на gotbolt).

Может вы что-то другое пишите? Например если написать return printf(«Hello World!\n»); — то он, разумеется, перестаёт менять…
o_O а ща меняет, хотя всегда было
int main() {
printf(«Hello World!\n»);
}
Понятно, что что-то менялось, но вроде копипаст только чистый был.
До исполнения кода main(...) еще много чего происходит.
Это гораздо интереснее, если честно :)
Но очень платформоспецифично.
Sign up to leave a comment.

Articles