Pull to refresh

Comments 82

Еще стоит рассказать о разнице между структурами в C и С++.
Не думаю, что это нужно тем кто пишет на C++.
Действительно, зачем? Если только для общего развития…
То, что описано в этой заметке в большей степени востребовано программистами на С, чем на С++. Драйверы и низкоуровневые библиотеки стараются писать на С. А раз в заголовок вынесено С++, то, наверное, имеет смысл рассказать о том насколько усовершенствовались (приблизились к объектам, получили дополнительный функционал) структуры в С++.
Боюсь, тогда выйдет заметка о классах и ООП) Сегодня уже поздно. Завтра тогда добавлю раздел чисто с отличиями структур Сишных от Плюсовых.
Это же Капитанство. В С++ структуры или POD типы или же классы, но с public доступом по умолчанию. Причем последние получаются если в структурах задавать методы особенно виртуальные или наследовать их друг от друга.
Ага, да, если бы всё было так просто (некоторые члены комитета стандартизации даже не помнят наизусть всех правил POD'ности). В C++11 даже standard layout структуры ввели, так как POD сочетает слишком много случаев использования, а концепция одна.
Ну не наследованная ни от чего структура с методами вполне себе PODнутая остается по способу хранения в памяти. Да и наследованная полиморфная вполне может такой быть ибо информацию о наследовании в рантайме спокойно можно выкинуть.
только, наверное, следует сказать, что упакованные структуры работают медленнее, чем неупакованные.

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

u16be вообще что-то очень нестандартное.
> упакованные структуры работают медленнее, чем неупакованные.
Да, так как не все процессоры поддерживают обращение к невыровненным данным.

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

Не факт, в небезызвестной книге «Веревка достаточной длины...», автор рекомендует использовать битовые поля:

«Некоторые люди утверждают, что второй пример
лучше, чем битовое поле, потому что здесь нет неявного сдвига, но
многие машины поддерживают команду проверки бита, которая устраняет
какую-либо потребность в сдвиге, который в случае своего использования
вызывает очень незначительные накладные расходы. Необходимость в
устранении ненужной путаницы обычно перевешивает подобные
соображения о снижении эффективности.»
У вас ошибка в расположении padding bytes для второй и третьей структуры.
Спасибо, поправил.
Мне всегда было интересно, зачем менять выравнивание структур. Оно же не зря установлено компилятором для платформы.
Ну как зачем, чтобы напрямую читать из сети или файла бинарные блобы в структуры вместо того, чтобы написать нормальный парсинг.
ага, а потом нарваться в самый неожиданный момент на LE / BE
Некоторые считают, что так они показывают свой высокий профессионализм и знания.
Причины могут быть разные. Компилятором установлено значение по умолчанию, что не значит, что оно в любой ситуации будет оптимальным. Кроме того, не гарантируется что оно будет одинаковое для всех компиляторов даже под данную платформу, а значит для интерфейсов нужно явно указывать выравнивание.
> не гарантируется что оно будет одинаковое для всех компиляторов даже под данную платформу, а значит для интерфейсов нужно явно указывать выравнивание.

В пределах одного ABI будет одинаковое, а больше ни для какой практической цели не требуется.
В идеальном мире наверное так и есть. А в реальном — поделитесь ссылкой на список ABI под все платформы? (начиная с windows)
Зачем вам список ABI? Если не совпадают ABI двух модулей вы в первую очередь не сможете вызвать функции/методы ваших межмодульных интерфейсов да хотя бы из-за разницы в name mangling или способа передачи аргументов.
Под windows для этого есть стандарты типа stdcall и т.п., но они не задают выравнивание.
За каждую pragma в коде нужно его автора на день сажать в котел с кипящим маслом.
Столкнулся с этим недавно, когда пытался кое-какой клиент скомпилировать на 64-битной платформе. Без выравнивания ничего не работало.
Если работаете с железом — иногда бывают такие структуры где порядок поменять нельзя, а некоторые поля меньшего размера располагаются выше полей большего размера из-за чего компилятор может добавить выравнивание которое вас не обрадует. (Из примеров — дескрипторные таблицы x86)
Да, кстати, для информации: «The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined.»

То есть гарантировать, что битовые поля всегда будут на одном и том же месте — нельзя. С другим компилятором/платформой всё может перестать работать, а виноват в этом будет не компилятор.
Как уже написал выше qehgt у Вас ошибка для второй и третей структур, так как short подавляющее большинство компиляторов выравнивают на границу 2-х байтов. Что касается битовых полей, то с ними вообще все сложно. Например, MS VC (и не только он) выравнивает размеры битовых полей описанных как int и unsigned int на 4 байта даже если в структуре одно поле из одного бита, а использование для описания битовых полей всяких short и прочих модификаторов вообще не стандартно и implementation defined.
Стандарт не определяет:
— The alignment of the addressable storage unit allocated to hold a bit-field
И отдает на откуп компиляторам:
— Whether a ‘‘plain’’ int bit-field is treated as a signed int bit-field or as an
unsigned int bit-field
— Allowable bit-field types other than _Bool, signed int, and unsigned int
— Whether atomic types are permitted for bit-fields
— Whether a bit-field can straddle a storage-unit boundary
— The order of allocation of bit-fields within a unit
— The alignment of non-bit-field members of structures. This should present
no problem unless binary data written by one implementation is read by another.
P.S. Это был комментарий автору исходного сообщения :)
Всё это ой как печально, но есть способ бороться с этим прямо из кода:
#pragma pack(push, 1)

Ох уж эти виндузятники, вам уже впрям писать библию «нету дрогой ОС кроме виндоуз и другого компилятора, кроме vc». :) Это, я к тому, что используя платфоро-зависимые фичи и пропраятные расширения языка, стоит как минимум на это указывать.

Кстати, хотя #pragma pack это и майкрософтовская примочка, в gcc можно включит ее поддержку и использовать вместо родной __attribute__ ((__packed__))
Странно, у меня на древней версии MinGW прагма пак работала без проблем.
Ещё стоит вспомнить, что битовые поля — это thread unsafe штука, про что обычно забывают, и, в отличие от обычных полей структуры, независимо обновлять их из потоков уже не получится.
Ну безопастно обновлять поля структуры можно только со времен c++11, до него битовые поля и поля структуры на одной линии огня. После c++11 проблема осталась только с полями, да.
UFO just landed and posted this here
В разделе «Порядок байт» в коде скорее всего будут проблемы на LE :) Т.е. на в т.ч. x86… в старших битах первого байта там будет header_length, а в младших ver. Тогда как на BE наоборот, т.е. так же как и должно быть в IP пакете.
Собственно во многих ОС можно посмотреть netinet/ip.h, как там описан заголовок IP пакета. Так что битовые поля тоже достаточно платформозависимые (об этом правда уже написали выше), в отличии кстати от логических операторов ;) Конечно если для них изначально подготовить данные.
Это всё, конечно, хорошо. Но, как уже правильно отметили в комментариях, непереносимо, а на некоторых платформах и попросту небезопасно. Из лично известных мне — SPARC весьма щепетильно относится к выравниванию данных, и генерит исключение SIGBUS в случае доступа к неправильно выравненным данным.
Спасаться от лишнего паддинга можно и без pragma pack. Рецепт прост — поля в структуре «сортируются» по размеру. В начале помещаются самые большие (uint64, uint32), в конце — самые маленькие — (uint8). Все вопросы с порядком байт должны (на мой взгляд) решаться в точке сериализации и десериализации данных (ну, например, при отправке в сеть и получения из сети), а сам этот процесс, по хорошему, должен производиться не сразу в структуру, а путём последовательного чтения данных из буфера и размещения в соответствующих полях в специфичном для платформы виде. Может быть это не так «просто» и «изящно» выглядит, зато надёжно, переносимо и расширяемо.
Когда Вы делаете #pragma pack(push, 1), Вы выигрываете несколько байтов и проигрываете несколько тактов процессора (процессор default архитектуры не станет работать с int32_t, чей адрес не выравнен на границу 4-х). Имхо, нет смысла этого делать, пока Вам не надо по какой-то причине ужать размер структуры. И даже если Вам таки надо ужать структуру, то, ИМХО, лучше руками перетасовать поля структуры, чем делать pragma pack.
UFO just landed and posted this here
1. С помощью pragma pack Вы проигрываете такты процессора.
2. Без pragma pack тоже можно выделить нужное количество памяти и прочитать в нее структуру из файла, если приложение компилируется с одними и теми же настройками компилятора, одним и тем же компилятором. Если приложение компилируется в несколько либ, с разными настройками выравнивания, то чтения из файла это меньшая из Ваших проблем.

Использовать pragma pack, ИМХО, есть смысл использовать только в двух случаях:
1. Вы храните очень много данных в файле и размер жесткого диска и/или оперативной памяти сильно ограничены. В таком случае, скорее всего, процессорное время тоже очень дорого, по этому тут классический трейдоф между памятью и процессорным временем.
2. У Вас несколько версий программы, которые компилируется с разными настройками выравнивания. В таком случае, ИМХО, лучше использовать сериализацию, как писал FlexFerrum.
3. (last but not least) вы работаете с регистрами оборудования
Операция загрузки «пары мегабайт» как правило однократна. Ее производительность при этом маловажная вещь, ее можно не оптимизировать вообще.
А вот потом, при обработке этих данных, адресовать массив структур, который на 4 не делится будет более чем затратно.

имхо это плохое решение выиграть N милисекунд там где это неважно, чтобы потом проиграть N процентов там где это важно.
Ааааарррргх. Если процессор не может работать с невыровненными данными, значит компилятор, скорее всего, раскидает это на много команд чтения-сдвига-слияния.
И откуда вы (и многие другие комментаторы) взяли вообще, что программист волен раскидывать данные как хочет? Есть протокол обмена, есть заголовки, их хочется красиво разбирать (да и в код глянуть быстрее, чем искать стандарт и назначения полей). Я согласен, что стандарты обычно пишут красиво, но 24-битные числа тоже бывают.
>>Когда Вы делаете #pragma pack(push, 1), Вы выигрываете несколько байтов и проигрываете несколько тактов процессора
>Ааааарррргх. Если процессор не может работать с невыровненными данными, значит компилятор, скорее всего, раскидает это на много команд чтения-сдвига-слияния.

Я про это и написал. О чем Вы пытаетесь спорить?

>И откуда вы (и многие другие комментаторы) взяли вообще, что программист волен раскидывать данные как хочет?
Это С++, детка, здесь могут и pragma pack написать, и delete this. Язык разрабатывался в расчете на то, что программист лучше знает. По этому, он дает возможность выстрелить себе в ногу. По этому, он дает возможность писать очень эффективный код для конкретного железа.
Нет, моё «Ааааарррргх ...» было к «процессор default архитектуры не станет работать с int32_t, чей адрес не выравнен на границу 4-х».

>Это С++, детка
Вот яркий пример: ru.wikipedia.org/wiki/%D0%9F%D0%B0%D0%BA%D0%B5%D1%82_IPv6#.D0.A4.D0.B8.D0.BA.D1.81.D0.B8.D1.80.D0.BE.D0.B2.D0.B0.D0.BD.D0.BD.D1.8B.D0.B9_.D0.B7.D0.B0.D0.B3.D0.BE.D0.BB.D0.BE.D0.B2.D0.BE.D0.BA. Расскажите авторам про то, что в своей структуре поменяете местами Flow Label и Traffic Class, т.к. так будет быстрее.
Не надо путать протоколы обмена данными с представлением данных в памяти конкретной программы, работающей на конкретной архитектуре.
Я перестал понимать, какую точку зрения Вы отстаиваете:
— pragma pack благо и его надо использовать всегда
— pragma pack вред и его нельзя использовать никогда
— pragma pack вред, но иногда его можно использовать
— pragma pack благо, но иногда лучше обойтись без него
— все любят котиков
— Единая Россия.

Я отстаиваю точку зрения, что раз человек использует pragma pack, значит ему это надо. И он представляет, к чему это приведёт.
А если не раскидает? Да и вообще, с какой стати компилятор должен раскидывать доступ к таким данным? Нет, может быть за последние 6 лет что-то и изменилось, но в середине 2000-ых сановские компиляторы таким точно не страдали.
Протокол обмена — это, обычно, хорошо стандартизированная и описанная штука. И данные, которые ходят по этому протоколу, совсем не обязательно должны «ложиться» на их бинарное представление в памяти. Более того, то, что они в данном конкретном случае ложатся — это большое везение. :) По этому при получении данных их таки лучше разобрать «ручками», и разложить по полям структуры, чем играть в рулетку и надеяться на то, что авось повезёт, и данные в память лягут так, как надо.
В моём случае повезло (вместе с первоначальным копированием из устройства), а универсальности не существует: всегда найдётся что-нибудь, что разрушит стройность. Вот я столкнулся с тем, что через PCI плохо записывать в 1 устройство меньше 4 байт (занулит остальное), а из другого читать больше 2 байт (тоже занулит). И да, в таком случае уже полагаться на компилятор не получается.
А вообще, ситуаций бесконечно много и в каждой свои методы удобнее. Можно лишь описать инструменты и *ожидаемую разработчиком* область применения.
Об том и речь. А по поводу «ожидаемой области применения»… У меня сложилось впечатление, что автор статьи сам слабо себе представляет цену тех советов, которые даёт. Такое впечатление сложилось вот почему. В первой части (про pragma pack) резюме такое: «Если вам мешает паддинг, выкиньте его с помощью прагмы!». И ни слова о том, зачем паддинг нужен, и почему от него надо избавляться только в самом крайнем случае. Из содержимого третьей части можно сделать вывод, что автор предлагает читать заголовок TCP/IP-пакета сразу в предложенную структуру. При этом нет ни слова о том, что, вообще говоря, структура эта не является POD-типом (потому что в ней присутствуют не-POD типы — те самые u16be), и такие манипуляции с ней не рекомендуются. Таким образом, в случае компиляции всего этого дела одним конкретным компилятором (Visual C++) может работать, а другом — нет.
UFO just landed and posted this here
То есть вот никаких других способов нет вообще, так?
UFO just landed and posted this here
Насколько я помню, у битовых полей наблюдались проблемы с совместимостью, как между компиляторами, так и архитектурами.

Хотя было бы приятно узнать, что я неправ.
довольно жесткий стандарт для индустриального ПО — MISRA — битовые поля ограничивает: RULE 111: Bit fields shall only be defined to be one of type unsigned int or signed int. Все остальное считается «implementation-defined type».

Часто для критических приложений использование битовых полей просто запрещают.
Ну, собственно, если для адекватного понимания конструкции в продакшен-коде требуется хорошенько покопаться в стандарте (или использование этой конструкции ведёт к UB/ID), то лучше такую конструкцию не использовать.
Ну, это не обязательно, портабельный код нужен не везде, даже для частично портабельного прецедент и читабельность важнее, ex:

$ grep -r "__packed" /usr/src/linux | wc -l
9075
Да я и не говорил, что исключений не бывает. :)
У меня сразу простой вопрос, как с переносимостью кода на другие архитектуры, не x86? ARM (седьмой и девятый), MIPS например?

за ARM9 и MIPS не скажу, но ARM7, емнип, только выравненные по 4-байтовой границе данные понимает. Cortex — побайтово
Это больше к компилятору вопрос, умеет ли он нормально разруливать особенности архитектуры. GCC, например, глючил для SPARC, как уже выше писали (сейчас может исправился).
Выше писали про SunCC (SunCC 5.5, если быть совсем точным) :)
если схемотехника камня физически не позволяет адресовать отдельные байты, никакие фишки компилятора тут не помогут
смотря что вы имеете в виду

реализовать обращение к невыровненному байтовому полю через, скажем, 32-битный read-modify-write компилятор может без особого труда, согласны?
Согласен. Я как раз о том, что если проц не имеет инструкции «загрузить байт по адресу… в регистр» или имеет, но адрес при этом должен быть кратным адресу слова (как раз вариант ARM7), то для манипуляции с байтом компилятору придется грузить все слово и использовать маски
Планирую в одной из следующих статей выложить идеальные, с моей точки зрения, структуры для работы с заголовками протоколов стека TCP/IP

Давай. А мы посмотрим, как оно заработает на PowerPC
Для начало надо будет посмотреть, как оно заработает на x86 :) Т.к. описанная в статье структура IP пакета соответствует IP протоколу как раз на процессорах с big endian, а на LE будет отличаться.
У меня она работает на LittleEndian. Нижняя структура, там где u16be
И какой же результат будет у этого кода?
IpHeader iphdr = {0};
iphdr.version = 4;
std::cou t<< std::hex << std::showbase << (int)*(uint8_t *)&iphdr << std::endl;

Почему в последних 4х битах первого байта вдруг оказалась версия IP протокола, когда она должна быть в первых 4х?
Можно увидеть ваш вывод? И назовите компилятор и платформу, пожалуйста.
В коде лишний пробел случайно поставил :)
А так вывод 0х4. Платформа например x86_64, компилятор gcc version 4.6.3 (Debian 4.6.3-1). Или специально проверил на под Windows x86, компилятор «Microsoft ® 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86» (правда пришлось stdint.h отсюда подсунуть msinttypes.googlecode.com/svn/trunk/stdint.h). Результаты идентичные. Да и собственно я же не просто так про это написал, выше уже писал про netinet/ip.h. В моем дистрибутиве в нем есть такие строчки:
struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif

Под другими ОС, которые поддерживают как LE так и BE платформы, и где такой файл в наличии, так же присутствует нечто подобное.
Спасибо, добавлю в статью что порядок выделения бит тоже зависит от порядка байт. Я действительно перепутал порядок. Надеюсь вскоре опубликовать статью с универсальными заголовками. Попытаюсь шаблонизировать битовые поля…
Я сказал только про первые два битовых поля, но далее то там есть еще, для флагов и смещения…
А может быть не стоит? В чём профит?
Хотелось бы услышать про магию u16be. Никогда не сталкивался.
Там ссылка на другую статью стоит
А чем не нравится htons(), ntohs(), htonl(), nthol(), да еще и на C++? Нужно изобрести свой велосипед по перетасовке бит?
Не нравится тем, что можно забыть это сделать. А обёрнутое классом — не надо помнить.
по перестановке байт, если быть точным
в свое время убил на это целый рабочий день — считывался поток данных по uart, а потом на считанный массив накладывался указатель на структуру и поидее сразу можно было таким образом удобно работать со считанными данными, но из за выравнивания после накладывания структуры — оказывался в полях какой то мусор — получалось — смотрю на структуру — вроде по размерам полей и порядку все сходится, смотрю на массив — вроде тоже, а вот в скомпилированном виде уже нифига не сходилось
Sign up to leave a comment.

Articles