Во-первых, вы можете правильно, переносимо и эффективно решить проблему выравнивания, используя, например, std::aligned_storage::value>::type вместо char[sizeof(int)] (или, если у вас нет C++ 11, может быть аналогичная функциональность для конкретного компилятора).
Даже если вы имеете дело со сложным POD, aligned_stored
и alignment_of
предоставят вам буфер, в который вы можете memcpy
вводить и извлекать POD, встраивать его и т. д.
В некоторых более сложных случаях вам нужно написать более сложный код, потенциально используя арифметику времени компиляции и статические переключатели на основе шаблонов и т. д., но, насколько я знаю, никто не придумал такой случай во время обсуждений C++11. это было невозможно справиться с новыми функциями.
Однако просто использовать reinterpret_cast
для случайного выровненного по символам буфера недостаточно. Давайте посмотрим, почему:
приведение переинтерпретировать указывает компилятору, что он может обрабатывать память в буфере как целое число
Да, но вы также указываете, что он может предположить, что буфер правильно выровнен для целого числа. Если вы лжете об этом, генерировать неработающий код можно бесплатно.
и впоследствии может выдавать инструкции, совместимые с целыми числами, которые требуют/предполагают определенные выравнивания для рассматриваемых данных.
Да, можно выпускать инструкции, которые либо требуют этих выравниваний, либо предполагают, что о них уже позаботились.
при этом единственными накладными расходами являются дополнительные чтения и сдвиги, когда ЦП обнаруживает, что адрес, который он пытается выполнить, ориентированные на выравнивание инструкции, на самом деле не выровнены.
Да, он может выдавать инструкции с дополнительными чтениями и сдвигами. Но он также может выдавать инструкции, которые их не выполняют, потому что вы сказали ему, что это не обязательно. Таким образом, он может выдать инструкцию «чтение выровненного слова», которая вызывает прерывание при использовании на невыровненных адресах.
Некоторые процессоры не имеют инструкции «чтение выровненного слова», а просто «читают слово» быстрее с выравниванием, чем без него. Другие могут быть настроены на подавление ловушки и вместо этого вернуться к более медленному «чтению слова». Но другие, такие как ARM, просто потерпят неудачу.
Если предположить, что выравнивание места в буфере, из которого будет выполняться приведение, не соответствует, то верно ли, что единственным решением этой проблемы является копирование байтов 1 на 1? Возможно, есть более эффективная техника?
Вам не нужно копировать байты 1 на 1. Вы можете, например, memcpy
каждую переменную одну за другой в правильно выровненное хранилище. (Это было бы копирование байтов 1 на 1, если бы все ваши переменные имели длину 1 байт, и в этом случае вы бы не беспокоились о выравнивании в первую очередь…)
Что касается приведения POD к char* и обратно с использованием специфичных для компилятора прагм... что ж, любой код, который полагается на специфичные для компилятора прагмы для корректности (а не, скажем, для эффективности), очевидно, не является корректным переносимым C++. Иногда «исправить с помощью g++ 3.4 или более поздней версии на любой 64-битной платформе с прямым порядком байтов с 64-битными двойниками IEEE» достаточно для ваших случаев использования, но это не то же самое, что действительно быть действительным C++. И вы, конечно, не можете ожидать, что он будет работать, скажем, с Sun cc на 32-битной платформе с обратным порядком байтов с 80-битными двойниками, а потом жаловаться, что это не так.
Для примера, который вы добавили позже:
// Experts seem to think doing the following is bad and
// could crash entirely when run on ARM processors:
buffer += weird_offset;
i = reinterpret_cast<int*>(buffer);
buffer += sizeof(int);
Эксперты правы. Вот простой пример того же самого:
int i[2];
char *c = reinterpret_cast<char *>(i) + 1;
int *j = reinterpret_cast<int *>(c);
int k = *j;
Переменная i
будет выровнена по некоторому адресу, кратному 4, скажем, 0x01000000. Итак, j
будет по адресу 0x01000001. Таким образом, строка int k = *j
выдаст инструкцию для чтения 4-байтового выровненного 4-байтового значения из 0x01000001. Скажем, на PPC64 это займет примерно в 8 раз больше времени, чем int k = *i
, но, скажем, на ARM произойдет сбой.
Итак, если у вас есть это:
int i = 0;
short s = 0;
float f = 0.0f;
double d = 0.0;
И вы хотите записать это в поток, как вы это делаете?
writeToStream(&i);
writeToStream(&s);
writeToStream(&f);
writeToStream(&d);
Как вы читаете обратно из потока?
readFromStream(&i);
readFromStream(&s);
readFromStream(&f);
readFromStream(&d);
Предположительно, какой бы поток вы ни использовали (будь то ifstream
, FILE*
, что угодно) есть в нем буфер, поэтому readFromStream(&f)
проверит, доступно ли sizeof(float)
байт, если нет, прочитает следующий буфер, а затем скопирует первые sizeof(float)
байт из буфер по адресу f
. (На самом деле, это может быть даже умнее — например, разрешено проверять, не находитесь ли вы близко к концу буфера, и если это так, выполнить асинхронное упреждающее чтение, если разработчик библиотеки подумал, что это будет хорошей идеей.) Стандарт не говорит, как он должен делать копию. Стандартные библиотеки не должны выполняться нигде, кроме реализации, частью которой они являются, поэтому ifstream
вашей платформы может использовать memcpy
или *(float*)
, или встроенный компилятор, или встроенный ассемблер — и он, вероятно, будет использовать то, что является самым быстрым на вашей платформе.
Итак, как именно невыровненный доступ поможет вам оптимизировать или упростить это?
Почти в каждом случае выбор правильного типа потока и использование его методов чтения и записи является наиболее эффективным способом чтения и записи. И, если вы выбрали поток из стандартной библиотеки, он также гарантированно будет правильным. Итак, у вас есть лучшее из обоих миров.
Если в вашем приложении есть что-то особенное, что делает что-то другое более эффективным, или если вы пишете стандартную библиотеку, то, конечно, вы должны пойти дальше и сделать это. Пока вы (и любые потенциальные пользователи вашего кода) знаете, где вы нарушаете стандарт и почему (и вы на самом деле оптимизируете вещи, а не просто делаете что-то, потому что «кажется, что это должно быть быстрее»), это совершенно разумно.
Кажется, вы думаете, что было бы полезно поместить их в какую-то "упакованную структуру" и просто написать это, но в стандарте С++ нет такой вещи, как "упакованная структура". Некоторые реализации имеют нестандартные функции, которые вы можете использовать для этого. Например, и MSVC, и gcc позволят вам упаковать вышеуказанное в 18 байтов на i386, и вы можете взять эту упакованную структуру и memcpy
ее, reinterpret_cast
ее в char *
для отправки по сети, что угодно. Но он не будет совместим с точно таким же кодом, скомпилированным другим компилятором, который не понимает специальные прагмы вашего компилятора. Он даже не будет совместим с родственным компилятором, таким как gcc для ARM, который упаковывает то же самое в 20 байт. Когда вы используете непереносимые расширения стандарта, результат не является переносимым.
person
abarnert
schedule
09.11.2012
std::aligned_storage<sizeof(int), std::alignment_of<int>::value>::type
вместоchar[sizeof(int)]
(или, если у вас нет C++11, могут быть аналогичные функции, специфичные для компилятора). - person abarnert   schedule 09.11.2012memcpy
, но вы решили, что это читерство, так что это не считается? - person abarnert   schedule 09.11.2012int
из файла или сокета, верно? - person abarnert   schedule 09.11.2012