Pull to refresh

Comments 67

Как обычно, обо всех ошибках и неточностях перевода прошу сообщать в ЛС или при помощи контактов, указанных в моем профиле. Так же прошу дать фидбек по верстке, особенно кода, так как в этой статье активно использовалось новая markdown-разметка. Спасибо.
Отличная статья :) А-то от старого С 89, когда читаешь глаза вытекают :)
Спасибо. Ссылку на оригинал этого текста дал мне один из читателей Хабра вместе с несколькими полезными замечаниями по тексту еще после публикации перевода оригинальной статьи «How To C in 2016». К сожалению, он использовал аккаунт в ВК (по всей видимости заброшенный или фейковый) и с того дня там больше не появлялся. Так что даже поблагодарить за наводку не могу. (Или смогу, если он это прочтет и опять выйдет на связь)
Ага, штука хорошая, самому набивать такое — тяжко. Я тут недавно озаботился обратной стороной стандартов — открыл для себя классные GNU extensions — anonymous union и struct. Жалко нет хорошего каталога расширений. Конечно, использовать вещи не из стандарта — плохо, но без указанных выше фич код становится крайне некрасивым…
Что значит «нет каталога»? Расширения языка C, Расширения языка C++

Что вы понимаете под «anonymous union и struct»? Что-нибудь подбное?
$ cat test1.c
#include <stdbool.h>

struct Node {
  union {
    float f;
    int i;
  };
  bool is_float;
};

Ну дык эта стандартная фича C/C++, причём тут расширения:
$ gcc -std=c11 -pedantic -c test1.c 
$ gcc -x c++ -std=c++98 -pedantic -c test1.c


Я очень люблю и уважаю расширения GCC, но, тем не менее рекомендую собирать код с ключами -std=c11 или -std=c++14. Хотите расширений — укажите на это явно в кода:
$ cat test.c
int foo(int x) {
  __extension__ ({
    switch (x) {
      case 0 ... 5:
        return 1;
      case 6 ... 8:
        return 2;
      default:
        return 0;
    }
  });
}
$ gcc -std=c89 -pedantic -c test.c

И вы будете понимать — где у вас стандартный код, а где расширения GNU C, и читающему будет легче.
Для С++ — это экстеншен и -peadntic будет ругаться на это (вот так: «warning: ISO C++ prohibits anonymous structs [-Wpedantic]), для С99 — это дозволенная фича и он ругаться, соотвественно не будет :) Вообще я набил несколько шишек в попытках сделать то, что хочу без оверхеда по памяти: www.stableit.ru/2016/02/union-bit-fields-c.html
Кто будет ругаться? На что? На стандартный код? Это ошибка компилятора, нужно исправить.

Я оба примера прогонял через -pedantic — ровно как написано. Никаких проблем.
let's code.

cat anonumous_struct.cpp 
#include <iostream>

struct outer {
    struct {
        int a;
    };
};

int main() {
    return 0;
}


Стандартный компилятор из ubutnu 14.04:
g++ -v 2>&1|grep "gcc version"
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04) 


g++ anonumous_struct.cpp  -pedantic 
anonumous_struct.cpp:6:5: warning: ISO C++ prohibits anonymous structs [-Wpedantic]
     };
     ^


Самый новый gcc 5.3.0:
/opt/fastnetmon/libraries/gcc530/bin/g++ -v 2>&1|grep version 
gcc version 5.3.0 (GCC) 


Результат:
/opt/fastnetmon/libraries/gcc530/bin/g++ anonumous_struct.cpp  -pedantic
anonumous_struct.cpp:6:5: warning: ISO C++ prohibits anonymous structs [-Wpedantic]
     };
     ^


Clang:
clang++ --version
Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)
Target: x86_64-pc-linux-gnu
Thread model: posix


Итог:
clang++ anonumous_struct.cpp -pedantic
anonumous_struct.cpp:4:5: warning: anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
    struct {
    ^
1 warning generated.



Я не верю в ошибку двух топовых С++ компиляторах одновременно. А в то, что Вы не правы — верю больше :) Если есть аргументы — прошу ссылку на стандарты, я в них плохо ориентируюсь и не могу сходу найти абзац описывающий данное поведение.
Нет у меня никакой ошибки. И ошибок в компиляторе — тоже нет :-)

Ну кто ж вам виноват, что вы так лихо сложили в одну кучу хорошо известную и давно используемую стандартную сущность (аннонимные объединения появившиеся в C++98) и расширение GCC (анонимные структуры, описанные вот тут). Я собственно когда увидел что вы эти, сильно разные, сущности перечисляете как одну очень удивился — потому и переспросил…
Я искренне предполагал, что они обе — расширение, был совершенно неправ.

Прогнал код с pedantic и получилось занятное:
anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
anonymous types declared in an anonymous union are an extension [-Wnested-anon-types]


То есть экстеншенов у меня аж два. Один — сам по себе анонимный struct, второй — анонимный struct внутри анонимного union (стандартного).

Хех. Столько фич на пустом месте :)
Хех. Столько фич на пустом месте :)
Ну дык. В вашем исходном примере их тоже два: uint16_t fragment_offset : 13 — это тоже расширение (причём я, как бы, понимаю и разработчиков стандарта тоже: вы тут написали, с одной стороны, что бит в поле 13, с другой — что 16… так сколько же их там, чёрт побери?)…

Напишите лучше unsigned fragment_offset : 13 — это будет и понятно и по стандарту…
Прочитал пример по ссылке. Ну да: анонимные структуры — это расширение, в стандарте есть только анонимные объединения. Ну так скажите про это компилятору и всё:
$ cat test.c
#include <stdint.h>

typedef union __attribute__((__packed__)) {
  __extension__ struct {
    uint16_t fragment_offset : 13,
    more_fragments_flag : 1,
    dont_fragment_flag : 1,
    reserved_flag : 1;
  };
  uint16_t fragmentation_details_as_integer;
} fragmentation_details_t;
$ gcc -std=c89 -pedantic -c test.c
$ clang -std=c89 -pedantic -c test3.c
Как я уже сказал: я очень люблю и уважаю расширения GCC. Но их нужно использовать аккуратно и вдумчиво. И код должен компилироваться в режиме ANSI C с -pendanticом!
Ага, тут верно. Ни один компилятор не ругается на анонимные union. Но вот в моем кейсе красиво обойтись без анонимного struct не получается.

Но сама пометка __extension__ — разве в стандарте? Вообще, довольно удобная фича, надо весь свой код переписать под pedantic :)
Тут странный финт ушами. __extension__, разумеется, в стандарте отсутствует. Но в стандарте (ещё начиная с C89 — и во всех последующих!) сказано следующее:
Each name that contains a double underscore (_ _) or begins with an underscore followed by an uppercase letter (2.11) is reserved to the implementation for any use
То есть написав два слеша подряд вы автоматически вышли за пределы действия стандарта и попали в область, описанную разработчиками GCC. Потому, кстати, __attribute__ не нужно метить специально, хотя он тоже отсутствует в стандарте.

Если вы посмотрите на стандартные заголовочные файлы GNU C, то увидите там кучу расширений GCC, но, разумеется, они должны со всякими -pedanticами собираться — для этого __extension__ и нужен.

Расширения GCC удобны (иначе зачем бы их создавали?), но, увы, малоизвестны. Потому сборка с -pedanticом и без ключа -std=gnuXX мне и кажется разумной — вы тем самым метите все места, которые нужно искать в документации на GCC, а не в документации на стандартный C/C++, что, в общем, просто удобно.
Да, согласен, увидь я что-то такое в чужом коде — удивился бы довольно сильно. Explicit better than implicit :) Спасибо, хорошая штука, приведу свой код в соотвествии с этим подходом.
Удачи. Обартите внимание на мой самый первый пример — с case и switch.

Я когда первый раз пытался обернуть его в __extension__ полдня пробился без успеха пытаясь совать его в разные места в функции. Чуть не сдался, а потом посмотрел на примеры в GNU LIBC. Оказалось: никакой магии. Просто пишите
__extension__({ ... }); 

тут ({... }) — это ещё одно расширение (самое первое, как я понимаю). Позволяет засунуть внутрь выражения что угодно (описания переменных, циклы и прочее… в C (но не в C++) — можно дажи функций там наописывать и там же их вызвать!). А уже внутри там — можно и другие расширения использовать.
А может осилите отдельную крутую статью по расширениям? :) Дабы такая чудная магия не прозябала! Ведь задача тут одна — описать как можно лучше то, что в машинном коде реализуется нэтивно. И когда смотришь решения — тот же мультикейс напрашивается сам собой…
Ну… крутую статью не обещаю, но немного как-то систематизировать использование расширений… попробую.
Как и обещал включил -pedantic и разметил везде, где используются расширения этот факт явно:

-                struct __attribute__((__packed__))  {
+                __extension__ struct __attribute__((__packed__))  {
Ну тут я даже не знаю что посоветовать: __attribute__ как бы уже подразумевает __extension__ (так как два подчёркивания — см. выше), с другой — как некоторая дополнительная документирующая пометка — тоже имеет право на существование. А вот -pedantic — это хорошо однозначно.

P.S. По-хорошему им бы нужно сделать градации типа -Wgcc5, -Wgcc6 и т.д.: тогда можно было бы врубать сразу самые сильные warning'и и -Werror не боясь того, что "со всем этим барахлом" оно перестанет собираться на будущих версиях gcc… Мечты, мечты...
А мне как делающему первые шаги в С хотелось бы тоже почитать =)
UFO just landed and posted this here
«Старый Си» исповедует принцип WYSIWYG (что видите — то и получите): никакой «скрытой» магии. Глядя на любую его конструкцию можно без труда понять что где происходит. Грубо говоря N байт на входе порождают не более 100N на выходе (после препроцессирования, разумеется). Можно на это смотреть как на «баг» или как на «фичу», но ломать этот подход — не стоит. Это будет уже совсем другой язык.

Правда в GCC уже есть одна фича, которая может приводить к «геометрическому» разбуханию кода — но она редко используется и на практике неопасна. defer — в этом смысле куда потенциально опаснее…
UFO just landed and posted this here
Вопрос не в "увеличении количества кода". Вопрос в "нелокальности". Глядя на какую-то конкретную точку в программе на C вы всегда можете понять — что тут происходит. Смотрите на if (a==b) { — видите: ага, из памяти читаем a, b, сравниваем… пять инструкций, одна и которых — goto. Видим "}" — тут, в общем, возможны варианты, но это либо jmp, либо ничего (в зависимости от того — это конец цикла или просто блока).

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

Всякие finally, defer, деструкторы и прочее эту локальность разрушают. Что у вас после этого порождает return или просто закрывающая фигурная скобка — одному богу ведомо! Нужно целое расследование проводить, чтобы это понять! Причём разные returnы в одной и той же функции — ведут себя по разному.

Это не хорошо и не плохо. Это просто по-другому. Сильно по другому — совсем не так, как в C!

Я понимаю, что вам хочется, чтобы C вёл себя "поудобнее". Но это вам станет удобнее! А всем тем, кто с С работает многие годы (а то и десятилетия) — станет неудобно!

Да, можно много чего добавить в С. И это много раз проделывалось. Получались всякие C++, Objective-C и так далее. Но всё это — уже не C! Это — другие языки! С совсем другими свойствами!

Я исренне рад тому, что разработчики C99, C11 (и, надеюсь, C22 и так далее) удержались от соблазна добавить всяких defer...
Если подразумеваются байты, задействуйте unsigned char. Если речь об октетах, выбирайте uint8_t.

Разве в байте не всегда 8 бит? В char может быть сколько угодно бит, но разве char — это синоним байта (а не символа)?
Все примитивные типы в Си являются платформозависимыми и могут иметь любой размер. И да, в байте может быть не 8 бит.
Про типы в С я в курсе. Но я никогда не видел и не слышал, чтобы где-то всерьез говорилось о байте не из 8 бит.
Да, в 60 или 70 были машины со странной длиной слова или 5-6-7 битами для представления символов. Но мне казалось, что слово байт является синонимом для слова октет.

Я в курсе, что и сейчас есть машины, где CHAR_BITS не равно 8, но ведь это размер char, а не размер байта… или нет? Я запутался.
Мой любимый пример — DSP. Можно найти архитектуру, где sizeof(int) == sizeof(short) == sizeof(char) == 1, при этом char имеет размер 32 бита. Просто потому что процессор не может работать с типами другой длины.

Октет — это ровно 8 бит.
Байт — это минимально адресуемая ячейка памяти. Да, в x86 они совпадают, но это не значит что они должны совпадать в других архитектурах.
Cray также был устроен. То же самое: sizeof(int) == sizeof(short) == sizeof(char) == 1, CHAR_BITS == 32.
Байт — это минимально адресуемая ячейка памяти.

А вы уверены, что это общепринятое определение?
Это то, что написано в стандартах C и C++. Так что «по умочанию» при обсуждении C и C++ должно использоваться именно оно.
Да, вы правы. Мне казалось, что стандарт С вообще не оперирует понятием байт.
К сожалению или к счастью — но оперирует. И байт — это именно размер переменной типа char.

Вы, в какой-то степени, правы: очень мало людей знаю как именно стандарт определяет байт. Большинство либо думают, что стандарт этим понятием не оперирует, либо думают что речь идёт про 8-битовый байт.

Так что определение действительно не вполне «общепринятое». Но, с другой стороны, если не опираться при обсуждении C/C++ на стандарт, то… на что тогда вообще опираться?
UFO just landed and posted this here
Многие вещи нельзя понять, когда только начинаешь писать на С. Ну не пишут в книгах, а примеры кода на С — обычно поганейшие, потому что обычно там С 89 со всеми вытекающими. А Вот 99й стандарт просто прелесть :)
UFO just landed and posted this here
Ну кому нужен сахар — пишут на С++, при прочих равных и более высоком удобстве он генерирует ровно такой же по эффективности машинный код. Лично я предпочитаю С++.
UFO just landed and posted this here
Скажу Вам как С/С++-шник — прелесть C++ — в его возможности использовать только то, что нужно, вплоть до подмножества С, тогда код будет абсолютно идентичным. Страуструп не раз говорил, что этот принцип будет сохраняться — платить только за то, что используешь, неиспользуемые фичи никаким образом не влияют на код. Лучше использовать с умом С++, особенно учитывая, что многие фичи даются абсолютно бесплатно — классы без виртуальных методов и с отключенными rtti идентичны структурам; constexpr, namespaces, перегрузка функций, операторов, значения по умолчанию для аргументов и пр.
UFO just landed and posted this here
> функциональщины

кхм
UFO just landed and posted this here
Да просто к термину придрался. «Функциональщиной» все-таки обычно называют вычисления без побочных эффектов, а вышеописанное — это «процедурщина» :).
UFO just landed and posted this here
Скажу вам как сишник С++ не умеет генерировать машинный код так же эффективно как С.

Да, так же не умеет, зачастую получается быстрее.

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

Почему для «вспомогательных» вещей лучше использовать C (или, по крайней мере, не использовать исключения и шаблоны)? Потому что кеш процессора не резиновый. Чем больше его себе отгрызёт какой-нибудь printf, тем меньше останется на основную задачу. Некоторые функции стандартной библиотеки, наоборот, используются где-нибудь во внутренних циклах (memcmp или memset), но тут, скорее, нужно всё аккуратно на ассемблере написать, чем шаблоны использовать…
К огромному сожалению на C++ (да ещё и с шаблонами!) очень часто пишут даже те вещи, для которых важен размер. Такие как стандартная библиотека или ядро.

Для встраиваемых систем? Согласен, там размер важен. Хотя у меня опыт написания на C++ под attiny весьма положительный. Да ещё и с шаблонами!
Важность размера для обычных настольных машин и серверов не столь очевидна.

Потому что кеш процессора не резиновый.

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

Чем больше его себе отгрызёт какой-нибудь printf, тем меньше останется на основную задачу.

Если у вас в основном цикле выполняется printf, то у вас и так IO сожрёт больше, чем RAM access.

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

И вот как-то так получилось, что даже тупые iostreams оказались быстрее fscanf("%s %s %d"), потому что fscanf выполняет разбор строки формата при каждом вызове, а у fstream формат разбора «закодирован» непосредственно в цепоче вызовов.

У fstream'а я, впрочем, не смог победить 10% времени внутри какой-то наркомании с локалями, и в итоге оптимальным оказалось вообще использовать mmap() + boost::string_ref, замена которым обычных std::string, кстати, выполнилась легко, просто и безопасно, спасибо шаблонам.

Некоторые функции стандартной библиотеки, наоборот, используются где-нибудь во внутренних циклах (memcmp или memset)

Ну и отлично. Компилятор у вас увидит в компил-тайме, что вы вызываете memcmp на типе длиной в 8 байт, и вместо гроздьев проверок в рантайме выберет подходящую специализацию вот прям здесь. И всё это портабельно и переносимо.

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

но тут, скорее, нужно всё аккуратно на ассемблере написать, чем шаблоны использовать…

Замучаетесь с ассемблером бегать и при выходе новой микроархитектуры или расширенного набора инструкций свой ассемблер переписывать. Давайте лучше это авторам компиляторов оставим?
Для встраиваемых систем?
Для небольших таких встраиваемых систем. С 64GiB памяти и 48 ядрами на борту.

Важность размера для обычных настольных машин и серверов не столь очевидна.
Ну как вам сказать. Есть ведь простой способ проверить: влючаем -Os — и получаем «заметную прибавку к пенсии». А разница между -O2 и -Os — это мелочь по сравнению с тем, что шаблоны могут сделать.

Хотя у меня опыт написания на C++ под attiny весьма положительный. Да ещё и с шаблонами!
А вот тут как раз проблем гораздо меньше: «движущихся частей» не так много, эффекты все заметить гораздо проще.

Весьма редко нужно запихивать в кеш процессора весь бинарник, обычно нужно запихнуть в кеш основной считающий цикл.
Зависит от задачи. Если у вас чисто счётная задача и там есть несколько очень горячих циклов — тогда может быть. А если у вас сервер с сотнями мегабайт кода? Из них вполне может быть «горячего кода» и мегабайт и два и три. Где вы столько кеша найдётё? Даже самый наираспоследний Хасвелл имеет 32KiB кеша первого уровня и 256KiB — второго. И это число нифига не растёт! Кеш нужно расходовать не бережно, а очень бережно.

Это памяти (это та, которая «new disk») у вас может быть много. Кеша же много не бывает!

Если у вас в основном цикле выполняется printf, то у вас и так IO сожрёт больше, чем RAM access.
Это с какого-такого перепугу? printf'у не обязательно вызываться на каждом «обороте» цикла. Он может сбрасывать «отчёт» раз в 1000 повторов (или раз в миллион). Но ваш код он будет из кеша успешно выкидывать в любом случае.

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

Компилятор — весьма тупая скотина. Скопировать 8 байт с помощью mov'а — он ещё может придумать. А вот использовать SSE4.2 — уже никак. Вернее он SSE4.2 научится использовать тогда, когда все будут уже об AVX1024 каком-нибудь говорить. Посмотрите на этот patch, посмотрите куда он применяется и подумайте — почему (обратите внимание на обратный адрес автора, кстати).

У fstream'а я, впрочем, не смог победить 10% времени внутри какой-то наркомании с локалями, и в итоге оптимальным оказалось вообще использовать mmap() + boost::string_ref, замена которым обычных std::string, кстати, выполнилась легко, просто и безопасно, спасибо шаблонам.
Ну дык. Вначале вы проигрываете кучу времени на шаблонах, потом с профайлером можете отыграть сколько-то назад. Но это вовсе не значит, что вы выиграете у C.

Из моего личного опыта: был у нас компонент, который являлся bottleneck'ом для некоего workload'а. И его разработчики тоже много раз оптимизировали — и это работало: грамотное применения профайлера, рефакторинги (которые были возможны из-за «правильной архитектуры») и прочее. Ускорили в конечном итоге почти в 4 раза. А потом пришёл я. С версией на C, которая была в 12 раз быстрее оригинальной :-) Причём я, в отличие от них, не пользовался ни профайлерами, ни «новёйшими технологиями». Правда там кодогенератор был использован — ну так кодогенераторы ещё в 60е появились.

Конечно есть и обратные примеры: Gold, к примеру, работает гораздо быстрее ld — ну так это за счёт того, что его-то писал человек, знающий не только C++. Он написан для того, чтобы быть быстрым изначально, а не для того, чтобы его можно было ускорить погоняв профилировщик :-)

То есть тот феномен, что программы на C++, как правило, медленнее, чем программы на C — это в основном психологический феномен, не технический. Но… он таки имеет место быть.
Для небольших таких встраиваемых систем. С 64GiB памяти и 48 ядрами на борту.

А таки зачем там?

Зависит от задачи. Если у вас чисто счётная задача и там есть несколько очень горячих циклов — тогда может быть. А если у вас сервер с сотнями мегабайт кода? Из них вполне может быть «горячего кода» и мегабайт и два и три.

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

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

Это с какого-такого перепугу? printf'у не обязательно вызываться на каждом «обороте» цикла. Он может сбрасывать «отчёт» раз в 1000 повторов (или раз в миллион). Но ваш код он будет из кеша успешно выкидывать в любом случае.

Так, стоп, а сишный код не будет выкидывать, что ли?

Компилятор — весьма тупая скотина.

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

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

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

Потом, кстати, я разделители пробовал искать что через memchr, что через std::find_if, и второй подход оказался чуточку быстрее — ЕМНИП было меньше всякой подготовки перед тем, как всякую векторную наркоманию по массиву из char'ов гонять. Компилятор соптимизировать больше может.

А потом пришёл я. С версией на C, которая была в 12 раз быстрее оригинальной :-)

А из-за чего был такой выигрыш, если не секрет?

То есть тот феномен, что программы на C++, как правило, медленнее, чем программы на C — это в основном психологический феномен, не технический. Но… он таки имеет место быть.

Не только психологический.

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

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

А случаи, когда незаинлайненный вызов внутри горячего цикла выгоднее, мне бы ещё посмотреть.
Сморите, ищите. Пример был дан выше. Что-что? Большой, сложный, неудобный? А кому сейчас легко: да, на игрушечных примерах всё работает так, как вы говорите. На больших, реальных — нет. Добро пожаловать в реальный мир!

Так, стоп, а сишный код не будет выкидывать, что ли?
Сишного кода будет меньше. И его влияние будет меньше. Мир — он не чёрно-белый. Тут и другие оттенки есть.

А из-за чего был такой выигрыш, если не секрет?
За счёт того, что, увы и ах, но шаблонная магия не исчезает из кода бесследно. Даже современные компиляторы не могут переработать структуры данных, они только с кодом работают. Там где версия на C++ имела всевозможные параметризованные типы в C'шной версии был простой int32_t с грамотно выверенными битовыми полями. Причём пересекающимися (то есть «есть тут стоит такой бит, то смотреть там, если не стоит — смотреть тут»). И потому попросту данных C'шная версия гоняла по шине гораздо меньше. Вот и всё. Ну ещё она не имела кучи шаблонных уровней индирекции, но я не уверен, что это сильно сработало.
UFO just landed and posted this here
О, а вот и любители C без аргументов пришли.

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

Мой опыт показывает, что написать на C++ небольшой кусочек, который на всех бенчмарках порвёт код на C — легко. А вот всю программу… или хотя бы достаточно большую систему… тут всё гораздо сложнее.
А стоит ли ему их тут давать? Мой опыт показывают, что разработчики на C++ очень часто переоцинивают выигрыш от подобных вещей и недооценивают возможные последствия.

А какие последствия?

Мой опыт показывает, что написать на C++ небольшой кусочек, который на всех бенчмарках порвёт код на C — легко.

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

А вот всю программу… или хотя бы достаточно большую систему… тут всё гораздо сложнее.

…нет, на плюсах вообще большие системы описывать проще.
А какие последствия?
«Рыхлые» структуры данных, «рыхлый» код — и невозможность что-либо с этим сделать на поздних стадиях. Причём проявляется это, зачастую, как раз на поздних стадиях: пока у вас всё мальнькое и помещается в кеш, всё работает, но когда у вас программа становится большой… в своё время Линус сказал: Trust me: every problem in computer science may be solved by an indirection, but those indirections are expensive. Pointer chasing is just about the most expensive thing you can do on modern CPU's.

Разработчики C++ в стремлении организовать свои структуры удобным для обработки через шаблоны образом часто об этом забывают.

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

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

Практически эту тенденцию зачастую пытаются обуздать путём создания разного рода Style Guide'ов, которые просто директивно запрещают некоторые возможности C++ использовать, но в результате получается что и те, кто мог бы их использовать эффективно этого делать после этого не могут.

В общем сложно всё с C++, очень сложно. Посмотрим как rust справится или может ещё что-нибудь. Пока неясно — можно ли на всём этом вообще писать большие системы и насколько это эффективно, но первые результаты обнадёживают…
Если вы про то, что код, который генерирует G++ хуже, чем то, что генерирует GCC — то ваши сведения устарели. На одинаковых программах разницы нет. На это ушло много лет, да, но это всё-таки случилось. И довольно давно. Где-то к концу нулевых :-)

Так что «самая быстрая» программа на C и «самая быстрая» программа на C++ сегодня будут иметь одинаковую скорость.

А вот «типичная»… тут всё гораздо, гораздо сложнее…
memcmp() это ещё более-менее. А вот strstr()…
Эти функции имеют короткие имена из-за трансляторов тех времён (когда эти функции создавались), которые больше 6 символов не понимали и не различали регистр.
пускай почитает ядро линукса

Ядро линукса, как и большинство программ GNU, написаны на достандартовом C (диалект K&R). Почитай новые программы типа Git'а, там увидишь нормальный, современный C.
Ну не пишут в книгах, а примеры кода на С — обычно поганейшие, потому что обычно там С 89 со всеми вытекающими. А Вот 99й стандарт просто прелесть

Это ты путаешь диалект K&R со стандартом C89. C89 и C99 практически ничем не отличаются по своему внешнему виду.
Первое издание книги «Язык программирования C» содержал примеры на том достандартовом диалекте, но есть второе издание книги, где примеры на ANSI С89.
Вы меня, конечно, извините, но это чушь. В частности, int – самый приемлемый тип целочисленных данных для текущей платформы. Если речь идет о быстрых беззнаковых целых, как минимум, на 16 битов, нет ничего плохого в использовании int (или можно ссылаться на опцию int_least16_t, которая прекрасно справится с функциями того же типа, но ИМХО это куда подробнее, чем оно того стоит).


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

Например, есть такой очень классный плеер, deadbeef. Он написан на С и вполне себе кроссплатформенный. Однако зачем-то его авторы решили придумать свой формат плейлистов, причём не текстовый, а бинарный. И в этом формате используются платформозависимые типы данных, с платформозависимым порядком байт. Писать программу не на C (конкретно — на Rust) для разбора плейлистов из-за этого было больно. Кроме того, я более чем уверен, что если deadbeef запустить, скажем на ARM, и скормить ему плейлист, сделанный им же, но на Linux x86_64, то он его не проглотит вообще.

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

В общем, если программа на C пишется исключительно для себя и на один раз/для одного конкретного применения, то, вероятно, платформозависимые типы использовать можно. Но если программа должна быть хоть сколько-то кроссплатформенной, то использование платформозависимых типов эквивалентно созданию минного поля из граблей с привязанными топорами. А уж если это библиотека, которая предполагает использование из других программ, то неиспользование фиксированных типов это просто неуважение к её пользователям.
Всё зависит от назначения кода. Если вы описываете структуру данных для обмена, как в вашем примере, то целесообразно использовать платформонезависимые типы, так как это обеспечивает совместимость. Если вы описываете какие-то абстрактные внутренние переменные, как, например, счётчик цикла от 1 до 10, то целесообразно использовать платформозависимые типы, так как это обеспечивает эффективность. Хороший кроссплатформенный код требует именно чёткого понимания, в каком случае требуется использовать платформонезависимый тип, а в каком – платформозависимый.
Но ведь этот вариант ничуть не лучше.

Если хотите подчеркнуть разницу типов, пишите:

ptrdiff_t diff = ptrOld — ptrNew;

Так ведь в оригинале считалась разница в адресе (что, собственно, и подразумевается при использовании ptrdiff_t), а у вас — количество элементов между указателями, зависящее от типа указателя (а для этого надо использовать тип size_t).
UFO just landed and posted this here
Only those users with full accounts are able to leave comments. Log in, please.