Комментарии 91
little_Xword / big_Xword из буста вполне устраивают.
Вообще для сети (хоть и неофициально) принято использовать BigEndian, почему кто-то юзает LittleEndian мне честно говоря не совсем понятно…
Я бы сказал, частенько встречаются структуры в которых перемежаются Little- и Big-Endian. Для того и был придуман этот шаблон)
Так об этом и статья. Проблема в том, что практически все процессоры оперируют с числами в формате little endian, поэтому WORD номер порта, записанный в исходном коде как 255, будет интерпретирован компилятором в x00FF, а сетевая подстстема ожидает его в формате xFF00, что ЦП и компилятор интерпретируют как 65280.
Писал как-то раз реализацию протокола ModBus over TCP/IP, намучался с этим порядком.
Писал как-то раз реализацию протокола ModBus over TCP/IP, намучался с этим порядком.
«Практически все» это как? :)
Хорошо, львиная доля процессоров, используемых в современных ПК и мобильных устройствах.
А разве ARM не иной?
ARM это Bi-Endian, честно скажу, что не знаю, можно ли переключать режим endianness в пользовательском режиме. Даже если можно, то вопрос в кодогенераторе используемого компилятора. В большинстве случаев локальные константы хранятся прямо в командах в бинарном коде приложения, не знаю, есть ли настройки или директивы компиляции endianness у компиляторов под ARM, позволяющие менять бинарный формат генерируемого кода.
Т.е. можно ли написать:
#pragma endian(push, bigendian)
portNumber = 21;
#pragma endian(pop)
В общем надо выяснить у ARM спецов.
Я имел ввиду нативные BigEndian процессоры, вроде PowerPC, у которых запись в коде 255 автоматически превращается в xFF00 в памяти.
Т.е. можно ли написать:
#pragma endian(push, bigendian)
portNumber = 21;
#pragma endian(pop)
В общем надо выяснить у ARM спецов.
Я имел ввиду нативные BigEndian процессоры, вроде PowerPC, у которых запись в коде 255 автоматически превращается в xFF00 в памяти.
Если я правильно помню, большинство процессоров сейчас Bi-Endian, выбор Endian осуществляется операционной системой при загрузке.
Вопрос производительности все-таки не стоит забывать, да и операция «перестановки» байт в действительности редко вызывается, а вот всякие сравнения с таким operator const T() будут работать долго (а нужны часто). Не скажу, что совсем плохое решение, но специфическое.
Решение проектировалось таким образом чтобы работать на разных аппаратных платформах.
идея понятна, реализация красивая, но скорость будет выше при использовании в нужных местах кучи макросов проверок и ntoh/hton-функций. конечно, если это критично.
Ну тогда как всегда)) Либо быстро — либо красиво.
Приходится балансировать между этими понятиями.
Приходится балансировать между этими понятиями.
Современный компилятор прекрасно понимает как оптимизировать такой код.
почти верно, циклы развернет, переменные заменит на константы и получится что-то вроде кода макроса htonl для конструктора или оператора присваивания и ntohl для оператора получения числового значения. Пока поленился лезть в асм, но на практике скорость выполнения в 2 раза хуже чем для конструкции out[u] = htonl(in[u]); (5.7с против 3.2)
или VS 2010 не такой уж современный компилятор?
или VS 2010 не такой уж современный компилятор?
Я не знаю, как вы тестировали. На win32 (если судить по msdn) htonl — вообще библиотечная функция, а не макрос, т.е. никакой высокой производительности она показать не может.
Покажите код, что ли.
Покажите код, что ли.
В статье есть ссылка на Wiki: ntohl(), htonl(), ntohs(), htons().
Там написано, что это набор функций, предусмотренный в стандарте POSIX.
Там написано, что это набор функций, предусмотренный в стандарте POSIX.
Я так вот тестировал:
pastebin.com/8tdEEDAC
запускал в Release
Результаты такие:
00:00:04.439545
00:00:03.156729
Т.е. htonl заметно медленнее «ручной» работы. При этом собственно никакой ручной оптимизации не делалось, компилятор и сам понял, чего от него хотят.
pastebin.com/8tdEEDAC
запускал в Release
Результаты такие:
00:00:04.439545
00:00:03.156729
Т.е. htonl заметно медленнее «ручной» работы. При этом собственно никакой ручной оптимизации не делалось, компилятор и сам понял, чего от него хотят.
вот так: codepad.org/mz1ZoeYc
соответственно авторская реализация: 5.2с
ntohl: 3.2
_byteswap_ulong: 0.8
под linux ntohl будет близок к _byteswap_ulong.
соответственно авторская реализация: 5.2с
ntohl: 3.2
_byteswap_ulong: 0.8
под linux ntohl будет близок к _byteswap_ulong.
Вот, адаптировал вашу версию к запуску на codepad.org: http://codepad.org/8GdiPBnI
В первом тесте должно быть так:
test[u % MAX_LEN] = u;
После чего получаем:
33296167; Time 1: 1312
33296167; Time 2: 2844
test[u % MAX_LEN] = u;
После чего получаем:
33296167; Time 1: 1312
33296167; Time 2: 2844
да, Вы правы, кривизну какую-то мерил.
вот так получается с исправлением.
33296167; Time 1: 1841ms (BigEndian)
33296167; Time 2: 3276ms (htonl)
33296167; Time 3: 858ms (_byteswap_ulong)
неплохо, получается что вызов библиотечной функции и правда съедает много времени.
вот так получается с исправлением.
33296167; Time 1: 1841ms (BigEndian)
33296167; Time 2: 3276ms (htonl)
33296167; Time 3: 858ms (_byteswap_ulong)
неплохо, получается что вызов библиотечной функции и правда съедает много времени.
А причем здесь вообще ntohl и ntohs? здесь же вообще другой функционал! ntohl и ntohs позволяют получить данные в сетевом порядке байт независимо от платформы, но получить данные в порядке байт обратном сетевому независимо от платформы с их помощью не получится, а здесь можно и так и так!
не так: ntohl это network-to-host, перевод BigEndian в платформенный (обычно Little Endian) формат.
а есть еще и htonl (host-to-network), обратное преобразование. Так что «функционал» один и тот же, вопрос в красоте и удобстве, ну и, соответственно, в потерях скорости. А они есть даже относительно ntohl, и просто огромны относительно специфичных для платформы функций _byteswap_ulong, bswap,…
а есть еще и htonl (host-to-network), обратное преобразование. Так что «функционал» один и тот же, вопрос в красоте и удобстве, ну и, соответственно, в потерях скорости. А они есть даже относительно ntohl, и просто огромны относительно специфичных для платформы функций _byteswap_ulong, bswap,…
Нет никакого еще и htonl. htonl и ntohl делают ровно одно и тоже: если платформа как Intel, то они переворачивают байты, если сетевой порядок байт является для платформы родным, то они ничего не делают. Таким образом, получить порядок байт обратный сетевому, не зная платформу, с их помощью невозможно.
ок, если формулировать задачу как «получить порядок байт обратный сетевому» то такой функции нет. вопрос только — зачем?
Вот функция, переводящая из сетевого в хостовый порядк (и обратно, поскольку это одно и тоже) не зависящая от платформы (на примере short, аналогично для остальных типов):
<< 8 — то же самое, что *256, | — то же самое, что + (в данном случае)
short net_to_host(short s) {
char const* bytes = &s;
return (static_cast<short const>(bytes[0]) << 8) | bytes[1];
}
<< 8 — то же самое, что *256, | — то же самое, что + (в данном случае)
>>ntohl и ntohs позволяют получить данные в сетевом порядке байт независимо от платформы, но получить данные в порядке байт обратном сетевому независимо от платформы с их помощью не получится
Перевод из сетевого в хостовый и из хостового в сетевой — одна и та же операция. Поэтому раз получается одна, получается и другая
Перевод из сетевого в хостовый и из хостового в сетевой — одна и та же операция. Поэтому раз получается одна, получается и другая
НЛО прилетело и опубликовало эту надпись здесь
С 64-битными типами не будет работать.
ps: так как «char << 32» это (с точки зрения языка) всё ещё int, а не 64-битное целое.
ps: так как «char << 32» это (с точки зрения языка) всё ещё int, а не 64-битное целое.
Почему бы не так, тогда вроде и с эффективностью особо вопросов не будет:
templateunion LittleEndian
{
unsigned char bytes[sizeof(T)];
T w;
…
LittleEndian(const LittleEndian & t) {
w = t.w;
}
templateunion LittleEndian
{
unsigned char bytes[sizeof(T)];
T w;
…
LittleEndian(const LittleEndian & t) {
w = t.w;
}
Это будет работать только для одного из шаблонов, того — кто соответствует текущему порядку байтов.
А почему так? Вроде бы не важно в каком они порядке, присваивание одной переменной другой переменной того же типа по-любому идентично побайтовому копированию.
Только считываться будет всегда значение Little-Endian (имеется ввиду текущая архитектура, просто LE значительно преобладает). А суть шаблона была хранить данные в указанном порядке байтов.
Я говорю только про то, что при копировании из переменной в переменную того же самого типа цикл не нужен.
Для копирования из базовых типов или из одного вашего типа в другой ваш тип цикл конечно нужен.
Для копирования из базовых типов или из одного вашего типа в другой ваш тип цикл конечно нужен.
Вы предлагаете заменить реализацию метода LittleEndian::operator=(), не так ли? А ведь BigEndian::operator=() заменить не получится, там цикл в другую сторону повернут… Итого на платформе Little-Endian можно изменить код классе LittleEndian. Сейчас код обоих классов работает под любой платформой. А вы предлагаете написать код, специализированный под конкретную платформу… Или я не так вас понял.
Идея такая:
1. заменить слово struct на union
2. Добавить T w; после массива — то есть поле базового типа, которое накладывается на массив.
3. В теле копирующих конструкторов
LittleEndian(const LittleEndian<T> & t) и
BigEndian(const BigEndian<T> & t)
вместо цикла написать w=t.w;
4. всё.
1. заменить слово struct на union
2. Добавить T w; после массива — то есть поле базового типа, которое накладывается на массив.
3. В теле копирующих конструкторов
LittleEndian(const LittleEndian<T> & t) и
BigEndian(const BigEndian<T> & t)
вместо цикла написать w=t.w;
4. всё.
Для одного из шаблонов работа будет неправильной. Так как вы будете делать это на архитектуре Little-Endian — то класс BigEndian перестанет правильно работать. Посмотрите внимательно, в классах разный код в этом месте…
Что же вы думаете, я не добьюсь чтобы вы меня поняли :-))
У Вас там написано:
и
И в чем разница?
У Вас там написано:
LittleEndian(const LittleEndian<T> & t)
{
for (unsigned i = 0; i < sizeof(T); i++)
bytes[i] = t.bytes[i];
}
и
BigEndian(const BigEndian<T> & t)
{
for (unsigned i = 0; i < sizeof(T); i++)
bytes[i] = t.bytes[i];
}
И в чем разница?
Я не понял сразу что вы имеете ввиду LittleEndian(const LittleEndian & t) и BigEndian(const BigEndian & t). Тут Вы правы, вполне можно заменить на копирование через тип T. Пожалуй я так и сделаю. Я думал речь идёт о конструкторах LittleEndian(const T &) и BigEndian(const T &). Раз уж делать копирование через T, то конечно нужно писать union, не кастовать же)) Спасибо ещё раз.
На свежую голову, спасибо мне говорить не за что, так как выигрыш в производительности небольшой, а вот с переносимостью теперь действительно могут быть проблемы, из-за выравнивания :-((
#pragma pack(1) вижу, но оно гарантирует что
однобайтовые переменные никогда не будут выравниваться, а вот про unsigned такой гарантии может и не быть…
#pragma pack(1) вижу, но оно гарантирует что
однобайтовые переменные никогда не будут выравниваться, а вот про unsigned такой гарантии может и не быть…
Сорри за опечатки, вернее незнание особенностей хабра, правильно вот так:
template<typename T>
union LittleEndian
{
unsigned char bytes[sizeof(T)];
T w;
…
LittleEndian(const LittleEndian & t) {
w = t.w;
}
T operator = (const T t)
{
reinterpret_cast<T&>(bytes) = t;
return t;
}
если хотим скорости, то почему бы не сделать так? зачем городить union'ы?
Ну вы же весь кайф обломали!!!
Зачем было так писать
когда можно написать так
template<typename T>
union LittleEndian
{
unsigned char bytes[sizeof(T)];
T w;
…
}
Зачем было так писать
template<typename T>
struct LittleEndian
{
union {
unsigned char bytes[sizeof(T)];
T w;
}
...
}
когда можно написать так
template<typename T>
union LittleEndian
{
unsigned char bytes[sizeof(T)];
T w;
…
}
В вашей реализиции
выведет:
Это так и задумывалось, или все-таки надо поправить operator ++(int)? (С BigEndian то же самое)
LittleEndian<unsigned int> a = 0;
std::cout << ++a << std::endl;
std::cout << a++ << std::endl;
выведет:
1
2
Это так и задумывалось, или все-таки надо поправить operator ++(int)? (С BigEndian то же самое)
Спасибо, что-то действительно странное там было с операторами ++ и --. Уже поправил, сейчас добавлю в статью.
Кроме того, operator++() и operator--() у BigEndian должны возвращать ссылку на LittleEndian, а на самом деле пытаются вернуть ссылку на BigEndian. Мой компилятор (g++ 4.4.5) на это ругается.
картинка зачетная, довольно долго думал что она значит, потом вспомнил классику, перечитал оригинал, и там 2 группы так и назывались big-endian и little-endian.
забавно.
забавно.
из названия думал, что статья будет «рассуждение на тему что лучше и почему» — а тут унылый код.
Работаю постоянно с big endian. Если надо что-то крутануть не выпендриваюсь и делаю так:
static inline u32 swap32(u32 w)
{
return ((w & 0xff000000) >> 24) |
((w & 0x00ff0000) >> 8) |
((w & 0x0000ff00) << 8) | ((w & 0x000000ff) << 24);
}
Работаю постоянно с big endian. Если надо что-то крутануть не выпендриваюсь и делаю так:
static inline u32 swap32(u32 w)
{
return ((w & 0xff000000) >> 24) |
((w & 0x00ff0000) >> 8) |
((w & 0x0000ff00) << 8) | ((w & 0x000000ff) << 24);
}
Зачем операций столько лишних? Выделение байтов по маске, сдвиг, ИЛИ…
inline void SwapUINT16(UINT16* Value)
{
__asm
{
mov EAX, Value
rol word ptr [EAX], 8
}
}
inline void SwapUINT32(UINT32* Value)
{
__asm
{
mov EAX, Value
mov DL, byte ptr [EAX]
mov BL, byte ptr [EAX + 3]
mov byte ptr [EAX], BL
mov byte ptr [EAX + 3], DL
rol word ptr [EAX + 1], 8
}
}
Конечно, не кроссплатформенно, но для x86 сойдет.
inline void SwapUINT16(UINT16* Value)
{
__asm
{
mov EAX, Value
rol word ptr [EAX], 8
}
}
inline void SwapUINT32(UINT32* Value)
{
__asm
{
mov EAX, Value
mov DL, byte ptr [EAX]
mov BL, byte ptr [EAX + 3]
mov byte ptr [EAX], BL
mov byte ptr [EAX + 3], DL
rol word ptr [EAX + 1], 8
}
}
Конечно, не кроссплатформенно, но для x86 сойдет.
Как использовать код для 64х битных чисел и по значению, думаю, очевидно.
А для того, что бы осуществить автоконверсию данных этими функциями, можно использовать не шаблонные классы-обертки для чисел каждого размера. Всего надо 6 классов для знаковых и беззнаковых чисел размером 2, 4 и 8 байт. Всё равно по хорошему шаблон кроме как для чисел не используется, нету в сетевых данных чисел длиннее 64 бита.
А для того, что бы осуществить автоконверсию данных этими функциями, можно использовать не шаблонные классы-обертки для чисел каждого размера. Всего надо 6 классов для знаковых и беззнаковых чисел размером 2, 4 и 8 байт. Всё равно по хорошему шаблон кроме как для чисел не используется, нету в сетевых данных чисел длиннее 64 бита.
К сожалению, одна платформа и один компилятор.
Ну компилятор не совсем один, просто Visual C++ совместимый. Но я уверен, что большинство компиляторов поддерживают ассемблерные вставки. В общем придется делать кучу #IFDEF'ов под нужные компиляторы и платформы, это да. Но по идее быстрее что-то придумать сложно.
Во-первых, ваш код платформозависим: он работает не на уровне представления данных в памяти а на уровне интерпретации данного представления средой исполнения. Соответственно, на BE платформах он работать не будет.
Во-вторых, ваш код не позволяет пребразовать BE в LE (или наоборот):
Во-третьих, endianness определяет порядок индивидуально адресуемых единиц в машинном слове: ваш код для целых занимающих более одного слова будет работать неверно.
Во-вторых, ваш код не позволяет пребразовать BE в LE (или наоборот):
#include <iostream>
int main() {
unsigned char be_unit[4] = {}; be_unit[3] = 1;
BigEndian<int> be(*((int *) &be_unit));
LittleEndian<int> le(be);
std::cout << (int) be << "\n"; // 16777216
std::cout << (int) le << "\n"; // 16777216
};
Во-третьих, endianness определяет порядок индивидуально адресуемых единиц в машинном слове: ваш код для целых занимающих более одного слова будет работать неверно.
Представление данных в памяти не зависит от манеры интерпретации их средой исполнения: конструкторы для классов BigEndian и LittleEndian должны реализовывать семантику побайтового копирования:
А операторы приведения иметь, соответственно вид:
LittleEndian::LittleEndian(const T * p) {
const unsigned char * pc = (const unsigned char*) p;
for (unsigned i = 0; i < sizeof(T); i++)
bytes[i] = *pc++;
};
BigEndian::BigEndian(const T * p) {
const unsigned char * pc = (const unsigned char*) p;
for (unsigned i = 0; i < sizeof(T); i++)
bytes[i] = *pc++;
};
А операторы приведения иметь, соответственно вид:
LittleEndian::operator const T() const {
return raw_value;
}
BigEndian::operator const T() const {
return raw_value;
}
Суть была как раз в том, чтобы данные хранились по-разному. Если операторы будут выглядеть как просто
return raw_value;
, значит данные лежат как LittleEndian и только.Это означает, что BE и LE-платформами данные будут интерпретироваться по-разному:
unsigned char raw[4] = {}; raw[3] = 1;
// Big-endian platform (ex. MAC OS X on PPC) output: 1
// Little-endian platform (ex. MAC OS X on PPC) output: 16777216
std::cout << *((__int32 *) &raw) << "\n";
>> LittleEndian le(be);
Объект Little-Endian создался из Big-Endain так, чтобы сохранилось значение, а не представление. Так и было задумано.
Объект Little-Endian создался из Big-Endain так, чтобы сохранилось значение, а не представление. Так и было задумано.
Я полагаю, что просто возникнет путанница с использованием данных абстракций ввиду того, что обычно нам не нужно знать конкретное представление endianness на платформе. Мы работаем с данными и они в host byte order, а конверсия данных необходима лишь при выполнении конкретных I/O операций.
Все дело в том, что Вы абсолютно правы) Шаблон именно так и работает. При считывании кастует к Т, а при записи сохраняет в определенном формате. Изначально планировался лишь оператор приведения к типу Т и оператор присваивания из типа Т. Остальные лператоры появились по мере надобности, и даже не все еще есть)
Вас не смущает, что приведённая вами реализация работает неверно?
Честное слово, я не пойму зачем так все усложнять. Исходя из моего опыта достаточно функций OSLittleEndianToHostOrderXxx/OsBigEndgianToHostOrderXxx и для конверсии обратно. Их бы просто сделать универсальными, чтобы не возникала необходимость указывать конкретный тип в имени — и этого вполне достаточно :)
Ну и оператор присваивания должен возвращать ссылку/константную ссылку на LittleEndian, BigEndian соответственно :)
Ну и оператор присваивания должен возвращать ссылку/константную ссылку на LittleEndian, BigEndian соответственно :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Little-Endian против Big-Endian