Little-Endian против Big-Endian

    Одно время приходилось постоянно работать с сетевыми данными и в особенности с заголовками сетевого и транспортного уровней модели OSI. Постоянно напрягал тот факт, что многие поля были в Big-Endian, а код выполнялся на архитектуре Little-Endian. Ну невозможно было постоянно вызывать ntohl(), htonl(), ntohs(), htons()...


    Автор фото: Ciroduran, источник фото: flickr.

    Пришла идея реализации следующей пары шаблонов: LittleEndian<T> и BigEndian<T>.

    Шаблон для порядка байтов LittleEndian:



    Шаблон для порядка байтов BigEndian:



    Используются шаблоны следующим образом:



    Осталось дать типам более симпатичные имена, например так:



    Теперь можно использовать типы через более короткие имена u16be, u32be

    Единственное, в этих шаблонах не хватает различных операторов +=, -=, *=, ++ и прочих. Операторы имеются в полной версии кода, в статье их приводить было нецелесообразно. Вот ссылки на полные исходные коды LittleBigEndian.h (см. новую ссылку ниже в UPDATE1) и LittleBigEndianNames.h.

    UPDATE1:
    Как был верно замечено пользователем qehgt в комментарии к посту, код не вполне корректен для 64-битных чисел. Прошу прощения за эту оплошность. Вот LittleBigEndian.h (см. новую ссылку ниже в UPDATE3), в котором эта ошибка исправлена.

    UPDATE2:
    Товарищ sic провел тесты скорости работы приведённых шаблонов по сравнению с функциями htohl(), htonl(), ntohs(), htons(). Тесты показали относительное повышение быстродействия при использовании приведенных шаблонов вместо вызова библиотечных функций. Спасибо, за проделанную работу.

    UPDATE3:
    Товарищ rekub увидел возможность повышения быстродействия конструктора копирования объекта. Он предложил использовать union для представления данных и как массива и как переменной типа T. Предложенные модификации были реализованы в следующем исходнике LittleBigEndian.h (см. новую ссылку ниже в UPDATE4). Спасибо!

    UPDATE4:
    Товарищ vadelve указал на ошибку в реализации операторов постфиксного и префиксного инкремента и декремента. Исправленная версия: LittleBigEndian.h.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 91

      +1
      little_Xword / big_Xword из буста вполне устраивают.
        0
        Можно ссылку на описание? Что-то не гуглится…
          0
          входит в библиотеку spirit.
        +1
        Вообще для сети (хоть и неофициально) принято использовать BigEndian, почему кто-то юзает LittleEndian мне честно говоря не совсем понятно…
          0
          Я бы сказал, частенько встречаются структуры в которых перемежаются Little- и Big-Endian. Для того и был придуман этот шаблон)
            0
            Так об этом и статья. Проблема в том, что практически все процессоры оперируют с числами в формате little endian, поэтому WORD номер порта, записанный в исходном коде как 255, будет интерпретирован компилятором в x00FF, а сетевая подстстема ожидает его в формате xFF00, что ЦП и компилятор интерпретируют как 65280.
            Писал как-то раз реализацию протокола ModBus over TCP/IP, намучался с этим порядком.
              0
              «Практически все» это как? :)
                0
                Хорошо, львиная доля процессоров, используемых в современных ПК и мобильных устройствах.
                  0
                  А разве ARM не иной?
                    0
                    ARM это Bi-Endian, честно скажу, что не знаю, можно ли переключать режим endianness в пользовательском режиме. Даже если можно, то вопрос в кодогенераторе используемого компилятора. В большинстве случаев локальные константы хранятся прямо в командах в бинарном коде приложения, не знаю, есть ли настройки или директивы компиляции endianness у компиляторов под ARM, позволяющие менять бинарный формат генерируемого кода.
                    Т.е. можно ли написать:
                    #pragma endian(push, bigendian)
                    portNumber = 21;
                    #pragma endian(pop)
                    В общем надо выяснить у ARM спецов.

                    Я имел ввиду нативные BigEndian процессоры, вроде PowerPC, у которых запись в коде 255 автоматически превращается в xFF00 в памяти.
                      0
                      Если я правильно помню, большинство процессоров сейчас Bi-Endian, выбор Endian осуществляется операционной системой при загрузке.
                        0
                        1) Нет, большинство нынче — little-endian.
                        2) Нет, это задаётся при начальном старте процессора. Если начальный загрузчик будет в «не том endianess'е», процессор прочитает кашу, а не правильный код.
            0
            Вопрос производительности все-таки не стоит забывать, да и операция «перестановки» байт в действительности редко вызывается, а вот всякие сравнения с таким operator const T() будут работать долго (а нужны часто). Не скажу, что совсем плохое решение, но специфическое.
              0
              Решение проектировалось таким образом чтобы работать на разных аппаратных платформах.
                +1
                идея понятна, реализация красивая, но скорость будет выше при использовании в нужных местах кучи макросов проверок и ntoh/hton-функций. конечно, если это критично.
                  0
                  Ну тогда как всегда)) Либо быстро — либо красиво.
                  Приходится балансировать между этими понятиями.
                    +1
                    Современный компилятор прекрасно понимает как оптимизировать такой код.
                      0
                      почти верно, циклы развернет, переменные заменит на константы и получится что-то вроде кода макроса htonl для конструктора или оператора присваивания и ntohl для оператора получения числового значения. Пока поленился лезть в асм, но на практике скорость выполнения в 2 раза хуже чем для конструкции out[u] = htonl(in[u]); (5.7с против 3.2)
                      или VS 2010 не такой уж современный компилятор?
                        0
                        Я не знаю, как вы тестировали. На win32 (если судить по msdn) htonl — вообще библиотечная функция, а не макрос, т.е. никакой высокой производительности она показать не может.

                        Покажите код, что ли.
                          0
                          В статье есть ссылка на Wiki: ntohl(), htonl(), ntohs(), htons().
                          Там написано, что это набор функций, предусмотренный в стандарте POSIX.
                            +1
                            Я так вот тестировал:
                            pastebin.com/8tdEEDAC

                            запускал в Release
                            Результаты такие:
                            00:00:04.439545
                            00:00:03.156729

                            Т.е. htonl заметно медленнее «ручной» работы. При этом собственно никакой ручной оптимизации не делалось, компилятор и сам понял, чего от него хотят.
                              0
                              вот так: codepad.org/mz1ZoeYc
                              соответственно авторская реализация: 5.2с
                              ntohl: 3.2
                              _byteswap_ulong: 0.8

                              под linux ntohl будет близок к _byteswap_ulong.
                                +1
                                Вот, адаптировал вашу версию к запуску на codepad.org: http://codepad.org/8GdiPBnI
                                  0
                                  ого, не знал что здесь можно еще и выполнять код, круто!

                                  только для бенчмаркинга это совершенно не годится, здесь разница между версиями в 7 раз, когда на нормальной машине всего в 2.
                                  0
                                  В первом тесте должно быть так:
                                  test[u % MAX_LEN] = u;

                                  После чего получаем:
                                  33296167; Time 1: 1312
                                  33296167; Time 2: 2844
                                    +1
                                    да, Вы правы, кривизну какую-то мерил.

                                    вот так получается с исправлением.
                                    33296167; Time 1: 1841ms (BigEndian)
                                    33296167; Time 2: 3276ms (htonl)
                                    33296167; Time 3: 858ms (_byteswap_ulong)

                                    неплохо, получается что вызов библиотечной функции и правда съедает много времени.
                                      0
                                      А я в свою очередь доволен, результатами проверок. Самому как-то не приходило в голову скорость протестировать, интуиция говорила, что компилятор тут нормально с этим справится — так и вышло. Спасибо Вам за труды. (Как заряд появится — плюсану)
                                  0
                                  А причем здесь вообще ntohl и ntohs? здесь же вообще другой функционал! ntohl и ntohs позволяют получить данные в сетевом порядке байт независимо от платформы, но получить данные в порядке байт обратном сетевому независимо от платформы с их помощью не получится, а здесь можно и так и так!
                                    0
                                    не так: ntohl это network-to-host, перевод BigEndian в платформенный (обычно Little Endian) формат.
                                    а есть еще и htonl (host-to-network), обратное преобразование. Так что «функционал» один и тот же, вопрос в красоте и удобстве, ну и, соответственно, в потерях скорости. А они есть даже относительно ntohl, и просто огромны относительно специфичных для платформы функций _byteswap_ulong, bswap,…
                                      0
                                      Нет никакого еще и htonl. htonl и ntohl делают ровно одно и тоже: если платформа как Intel, то они переворачивают байты, если сетевой порядок байт является для платформы родным, то они ничего не делают. Таким образом, получить порядок байт обратный сетевому, не зная платформу, с их помощью невозможно.
                                        +1
                                        ок, если формулировать задачу как «получить порядок байт обратный сетевому» то такой функции нет. вопрос только — зачем?
                                          0
                                          Вам привести пример формата данных, в котором жестко задан порядок байт и этот порядок байт обратный сетевому? Ну GIF, например. Или вы это к тому, что все платформы, у которых сетевой порядок байт родной давно умерли? — тогда, конечно, да, незачем.
                                          0
                                          Вот функция, переводящая из сетевого в хостовый порядк (и обратно, поскольку это одно и тоже) не зависящая от платформы (на примере short, аналогично для остальных типов):

                                          short net_to_host(short s) {
                                              char const* bytes = &s;
                                              return (static_cast<short const>(bytes[0]) << 8) | bytes[1];
                                          }


                                          << 8 — то же самое, что *256, | — то же самое, что + (в данном случае)
                                            0
                                            тут у Вас получился ntohs, он же htons.
                                        0
                                        >>ntohl и ntohs позволяют получить данные в сетевом порядке байт независимо от платформы, но получить данные в порядке байт обратном сетевому независимо от платформы с их помощью не получится
                                        Перевод из сетевого в хостовый и из хостового в сетевой — одна и та же операция. Поэтому раз получается одна, получается и другая
                          • UFO just landed and posted this here
                              0
                              Я вот не припомню никакой путаницы
                              +4
                              С 64-битными типами не будет работать.

                              ps: так как «char << 32» это (с точки зрения языка) всё ещё int, а не 64-битное целое.
                                0
                                Похоже код:
                                t |= bytes[i] << (i << 3);
                                следует заменить на:
                                t |= T(bytes[i]) << (i << 3);
                                Сейчас поправлю в статье.
                                  +1
                                  Да, так правильнее.
                                    +1
                                    Спасибо. Как заряд появится, обязательно отблагодарю)
                                0
                                Почему бы не так, тогда вроде и с эффективностью особо вопросов не будет:

                                templateunion LittleEndian
                                {
                                unsigned char bytes[sizeof(T)];
                                T w;



                                LittleEndian(const LittleEndian & t) {
                                w = t.w;
                                }
                                  0
                                  Это будет работать только для одного из шаблонов, того — кто соответствует текущему порядку байтов.
                                    0
                                    А почему так? Вроде бы не важно в каком они порядке, присваивание одной переменной другой переменной того же типа по-любому идентично побайтовому копированию.
                                      0
                                      Только считываться будет всегда значение Little-Endian (имеется ввиду текущая архитектура, просто LE значительно преобладает). А суть шаблона была хранить данные в указанном порядке байтов.
                                        0
                                        Я говорю только про то, что при копировании из переменной в переменную того же самого типа цикл не нужен.

                                        Для копирования из базовых типов или из одного вашего типа в другой ваш тип цикл конечно нужен.

                                          0
                                          Вы предлагаете заменить реализацию метода LittleEndian::operator=(), не так ли? А ведь BigEndian::operator=() заменить не получится, там цикл в другую сторону повернут… Итого на платформе Little-Endian можно изменить код классе LittleEndian. Сейчас код обоих классов работает под любой платформой. А вы предлагаете написать код, специализированный под конкретную платформу… Или я не так вас понял.
                                            0
                                            Идея такая:
                                            1. заменить слово struct на union
                                            2. Добавить T w; после массива — то есть поле базового типа, которое накладывается на массив.
                                            3. В теле копирующих конструкторов
                                            LittleEndian(const LittleEndian<T> & t) и
                                            BigEndian(const BigEndian<T> & t)
                                            вместо цикла написать w=t.w;
                                            4. всё.
                                              0
                                              Для одного из шаблонов работа будет неправильной. Так как вы будете делать это на архитектуре Little-Endian — то класс BigEndian перестанет правильно работать. Посмотрите внимательно, в классах разный код в этом месте…
                                                0
                                                Что же вы думаете, я не добьюсь чтобы вы меня поняли :-))
                                                У Вас там написано:
                                                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];
                                                }

                                                И в чем разница?
                                                  +1
                                                  Я не понял сразу что вы имеете ввиду LittleEndian(const LittleEndian & t) и BigEndian(const BigEndian & t). Тут Вы правы, вполне можно заменить на копирование через тип T. Пожалуй я так и сделаю. Я думал речь идёт о конструкторах LittleEndian(const T &) и BigEndian(const T &). Раз уж делать копирование через T, то конечно нужно писать union, не кастовать же)) Спасибо ещё раз.
                                                    0
                                                    На свежую голову, спасибо мне говорить не за что, так как выигрыш в производительности небольшой, а вот с переносимостью теперь действительно могут быть проблемы, из-за выравнивания :-((

                                                    #pragma pack(1) вижу, но оно гарантирует что
                                                    однобайтовые переменные никогда не будут выравниваться, а вот про unsigned такой гарантии может и не быть…
                                                      0
                                                      Что-то я не представляю ситуации с неправильным выравниванием. Есть контрпример?)
                                    0
                                    Сорри за опечатки, вернее незнание особенностей хабра, правильно вот так:

                                    template<typename T>
                                    union LittleEndian

                                    {
                                    unsigned char bytes[sizeof(T)];
                                    T w;



                                    LittleEndian(const LittleEndian & t) {
                                    w = t.w;
                                    }
                                      0
                                          T operator = (const T t)
                                          {
                                              reinterpret_cast<T&>(bytes) = t;
                                              return t;
                                          }
                                      


                                      если хотим скорости, то почему бы не сделать так? зачем городить union'ы?
                                        0
                                        упс, конечно же
                                        BigInteger<T> &operator = (const BigInteger<T> &t)
                                        {
                                          reinterpret_cast<T&>(bytes) = reinterpret_cast<T&>(t.bytes);
                                          return *this;
                                        }
                                        
                                          0
                                          union был нужен как раз чтобы не вылезал жуткий reinterpret_cast
                                          0
                                          Так нельзя делать.

                                          Будут проблемы на системах со строгими требованиями к выравниванию.
                                        0
                                        Ну вы же весь кайф обломали!!!

                                        Зачем было так писать
                                        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;

                                        }

                                          0
                                          Просто потом возможны траблы при forward declaration типа:
                                          template<typename T>
                                          struct LittleEndian;
                                          Достаточно того что, он ругается если спутать class/struct.
                                          Если к этому списку добавится ещё и union…
                                            0
                                            ok, согласен, возможно это уже ненужный изыск.
                                          +1
                                          В вашей реализиции

                                          LittleEndian<unsigned int> a = 0;
                                          std::cout << ++a << std::endl;
                                          std::cout << a++ << std::endl;

                                          выведет:
                                          1
                                          2

                                          Это так и задумывалось, или все-таки надо поправить operator ++(int)? (С BigEndian то же самое)
                                            0
                                            Спасибо, что-то действительно странное там было с операторами ++ и --. Уже поправил, сейчас добавлю в статью.
                                              0
                                              Кроме того, operator++() и operator--() у BigEndian должны возвращать ссылку на LittleEndian, а на самом деле пытаются вернуть ссылку на BigEndian. Мой компилятор (g++ 4.4.5) на это ругается.
                                                0
                                                Это тоже заметил при правке ++ и --, спасибо.
                                              +2
                                              картинка зачетная, довольно долго думал что она значит, потом вспомнил классику, перечитал оригинал, и там 2 группы так и назывались big-endian и little-endian.
                                              забавно.
                                              –1
                                              из названия думал, что статья будет «рассуждение на тему что лучше и почему» — а тут унылый код.

                                              Работаю постоянно с big endian. Если надо что-то крутануть не выпендриваюсь и делаю так:

                                              static inline u32 swap32(u32 w)
                                              {
                                              return ((w & 0xff000000) >> 24) |
                                              ((w & 0x00ff0000) >> 8) |
                                              ((w & 0x0000ff00) << 8) | ((w & 0x000000ff) << 24);
                                              }
                                                +1
                                                Ну мне показалось, что через шаблон удобнее. А тесты показали, что он ещё и быстрее.
                                                  0
                                                  Зачем операций столько лишних? Выделение байтов по маске, сдвиг, ИЛИ…

                                                  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 сойдет.
                                                    0
                                                    Как использовать код для 64х битных чисел и по значению, думаю, очевидно.
                                                    А для того, что бы осуществить автоконверсию данных этими функциями, можно использовать не шаблонные классы-обертки для чисел каждого размера. Всего надо 6 классов для знаковых и беззнаковых чисел размером 2, 4 и 8 байт. Всё равно по хорошему шаблон кроме как для чисел не используется, нету в сетевых данных чисел длиннее 64 бита.
                                                      0
                                                      В своей следующей статье я расскажу что еще можно туда класть))
                                                      0
                                                      К сожалению, одна платформа и один компилятор.
                                                        0
                                                        Ну компилятор не совсем один, просто Visual C++ совместимый. Но я уверен, что большинство компиляторов поддерживают ассемблерные вставки. В общем придется делать кучу #IFDEF'ов под нужные компиляторы и платформы, это да. Но по идее быстрее что-то придумать сложно.
                                                          0
                                                          При генерации 64-битного кода Visual C+ не поддерживает ассемблерные вставки. Поэтому их не стоит использовать, даже если очень хочется.
                                                    +2
                                                    Во-первых, ваш код платформозависим: он работает не на уровне представления данных в памяти а на уровне интерпретации данного представления средой исполнения. Соответственно, на BE платформах он работать не будет.

                                                    Во-вторых, ваш код не позволяет пребразовать 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 определяет порядок индивидуально адресуемых единиц в машинном слове: ваш код для целых занимающих более одного слова будет работать неверно.
                                                      +2
                                                      Представление данных в памяти не зависит от манеры интерпретации их средой исполнения: конструкторы для классов 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;
                                                      }
                                                      
                                                        0
                                                        Суть была как раз в том, чтобы данные хранились по-разному. Если операторы будут выглядеть как просто return raw_value;, значит данные лежат как LittleEndian и только.
                                                          +1
                                                          Это означает, что 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";
                                                          
                                                            0
                                                            Так шаблон предназначался для работы с данными, которые уже записаны в одном из форматов. Шаблон позволяет работать с данными, указывая в каком формате их хранить.
                                                      0
                                                      >> LittleEndian le(be);
                                                      Объект Little-Endian создался из Big-Endain так, чтобы сохранилось значение, а не представление. Так и было задумано.
                                                        0
                                                        Каким образом, в таком случае, вы получаете little-endian из big-endian?
                                                        0
                                                        Я полагаю, что просто возникнет путанница с использованием данных абстракций ввиду того, что обычно нам не нужно знать конкретное представление endianness на платформе. Мы работаем с данными и они в host byte order, а конверсия данных необходима лишь при выполнении конкретных I/O операций.
                                                          0
                                                          Все дело в том, что Вы абсолютно правы) Шаблон именно так и работает. При считывании кастует к Т, а при записи сохраняет в определенном формате. Изначально планировался лишь оператор приведения к типу Т и оператор присваивания из типа Т. Остальные лператоры появились по мере надобности, и даже не все еще есть)
                                                        0
                                                        Вас не смущает, что приведённая вами реализация работает неверно?
                                                          0
                                                          Комментарий относится, к вышележащей ветке.
                                                            0
                                                            Будьте добры, приведите контр-пример неправильной работы.
                                                              0
                                                              Внимательно перечитайте первый комментарий вышележащей ветки.
                                                            0
                                                            Честное слово, я не пойму зачем так все усложнять. Исходя из моего опыта достаточно функций OSLittleEndianToHostOrderXxx/OsBigEndgianToHostOrderXxx и для конверсии обратно. Их бы просто сделать универсальными, чтобы не возникала необходимость указывать конкретный тип в имени — и этого вполне достаточно :)

                                                            Ну и оператор присваивания должен возвращать ссылку/константную ссылку на LittleEndian, BigEndian соответственно :)
                                                              0
                                                              Пардон, парсер сьел < и >. В любом случае, комментарий по поводу оператора присваивания читать недействительным, недоглядел прототип. Да и генерируемого по умолчанию должно быть достаточно.

                                                            Only users with full accounts can post comments. Log in, please.