На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Правила раздела FAQ в группе разделов С++.
1. Раздел FAQ предназначен для публикации готовых статей.
2. Здесь нельзя задавать вопросы, для этого существуют соответствующие разделы:
Чистый С++
Visual C++ / MFC / WTL / WinApi
Borland C++ Builder
COM / DCOM / ActiveX / ATL
Сопутствующие вопросы
3. Внимание, все темы и сообщения в разделе премодерируются. Любое сообщение или тема будут видны остальным участникам только после одобрения модератора.
Модераторы: B.V., Qraizer
  
> Выравнивание.
    Очень часто приходится слышать про термин "выравнивание структур".
    Дело в том, термин этот во многом результат недоразумения: процессор
    умеет работать только с элементарными типами, но не структурами.

    Однако этот термин имеет право на существование в контексте подсчёта
    размеров сложных структур. Вот его то мы и рассмотрим.

    Условимся, что размеры элементарных типов у нас следующие:

    ExpandedWrap disabled
      sizeof(char) = 1
      sizeof(bool) = 1
      sizeof(short) = 2
      sizeof(int) = 4
      sizeof(double) = 8


    выравнивание элементарного типа равно его sizeof
    выравнивание структуры равно максимальному выравниванию
    его составляющих, если только не выставлена опция компилятора
    "structure alignment". В этом случае выравнивание структуры будет
    НЕ БОЛЬШЕ (а возможно, меньше) выставленного компилятором значения.
    Выравнивание массива равно выравниванию одного элемента этого массива.

    Пример:
    подсчитать sizeof() следующей структуры:

    ExpandedWrap disabled
      struct A
      {
        bool a;
        char b;
        int c;
      };

    При подсчёте удобно заполнять следующую таблицу.

    (поле)     (начальная позиция)    (выравнивание)  (выравнивание вверх)
    


    Ввыравнивание вверх - это минимальное число, которое делится на выравнивание, и
    которое больше или равно начальной позиции.
    Начальная позиция первого элемента равна 0
    Начальная позиция каждого последующего элемента равна выравниванию вверх предыдущего
    элемента + sizeof(предыдущего элемента).

    заполним её:

    
    поле    начальная позиция   выравнивание    выравнивание вверх
    a            0                       1                0
    b            0 + 1 = 1               1                1
    с            1 + 1 = 2               4                4
    struct       4 + 4 = 8               4                8
    




    sizeof(A) = 8. Выравнивание по умолчанию - 4.


    Посчитаем более сложный вариант.

    ExpandedWrap disabled
      struct A
      {
        short a[3];
        struct B
        {
          int e;
          bool f[3];
        } b;
        double c;
        int d;
      };

    заполним таблицу.

    сначала для структуры B
    поле    начальная позиция   выравнивание    выравнивание вверх
    e           0                       4                   0
    f           0 + 4 = 4               1                   4
    struct      4 + 3 * 1 = 7           4                   8
    


    sizeof (B) = 8. Выравнивание = 4.

    А теперь для структуры А
    поле    начальная позиция   выравнивание    выравнивание вверх
    a         0                    2                0
    b         0 + 3 * 2 = 6        4                8
    c         8 + 8 = 16           8                16
    d         16 + 8 = 24          4                24
    struct    24 + 4 = 28          8                32
    

    sizeof(A) = 32, выравнивание A = 8.

    Обратим внимание, что в случае жёсткого выставления нужной опции компилятора, допустим, в 2,
    мы бы получили совершенно иную картину, т.к. в графе "выравнивание" мы бы не получали числа
    больше 2.

    Алгоритм простейший.
    Как видно, результирующий sizeof() структуры может заметно отличаться от суммы sizeof()-ов
    входящих в него элементов. Это - результат того, что кроме "полезных" данных в структурах
    хранится так же "мусор", единственная задача которого - поддерживать правильное выравнивание.

    Этот мусор может сыграть злую шутку, когда, например, 2 разные программы, собранные с различными
    опциями компилятора "structure alignment" могут по разному интерпретировать одни и те же двоичные
    данные, что может привести к довольно страшным последствиям. В следствии этого рекомендуется
    перед сохранением и чтением структур выставлять опцию компилятора #pragma pack (push, 1) а после
    чтения - #pragma pack (pop). Почему бы всё время не выставлять опцию копилятора "выравнивание структур"
    в значение 1, это бы существенно уменьшило нам побочные эффекты? Делать этого не следует ни в коем случае,
    т.к. это может существенно замедлить работу программы.

    Полезные функции.
    Есть несколько полезных функций:
    1) sizeof() - вычисляет фактический размер типа.

    2) offsetof(struct name, member name) - вычисляется смещение члена
    стурктуры относительно начала (начальная позиция).

    3) Полезный шаблон, вычисляющий текущее выравнивание структуры:

    ExpandedWrap disabled
      template <typename T>
      class AlgOf
      {
          struct S{
              char dummy;
              T f;
          };
      public:
          enum {Result = sizeof(S) - sizeof(T)};
      };


    (мдя, таблички малость кривоваты получились :)
    Сообщение отредактировано: BugHunter -
      Цитата BugHunter @
      (мдя, таблички малость кривоваты получились

      отредактируй с обрамлением в
      [pre]
      поле    начальная позиция   выравнивание    выравнивание вверх
      a            0                       1                0
      b            0 + 1 = 1               1                1
      с            1 + 1 = 2               4                4
      struct       4 + 4 = 8               4                8
      
      Сообщение отредактировано: Sazabis -
        А вот ещё кусочек из книжки Джефри Рихтера "Создание эффективных Win-32 приложений с учётом специфики 64 разрядной версии Windows", на эту же тему. Очень занимательно. :yes:

        Выравнивание данных
        Здесь мы отвлечемся от виртуального адресного пространства процесса и обсудим такую важную тему, как выравнивание данных. Кстати, выравнивание данных — не столько часть архитектуры памяти в операционной системе, сколько часть архитектуры процессора.

        Процессоры работают эффективнее, когда имеют дело с правильно выровненными данными. Например, значение типа WORD всегда должно начинаться с четного адреса, кратного 2, значение типа DWORD - с четного адреса, кратного 4, и т.д. При попытке считать невыровненные данные процессор сделает одно из двух: либо возбудит исключение, либо считает их в несколько приемов.

        Вот фрагмент кода, обращающийся к невыровненным данным:

        ExpandedWrap disabled
          VOID SomeFunc(PVOID pvDataBuffer)
          {
              // первый байт в буфере содержит значение типа BYTE
              char с = * (PBYTE) pvDataBuffer;
           
              // увеличиваем указатель для перехода за этот байт
              pvDataBuffer = (PVOID)((PBYTE) pvDataBuffer + 1);
           
              // байты 2-5 в буфере содержат значение типа DWORD
              DWORD dw = * (DWORD *) pvDataBuffer;
           
              // на процессорах Alpha предыдущая строка приведет к исключению
              // из-за некорректного выравнивания данных
              ...
           
          }


        Очевидно, что быстродействие программы снизится, если процессору придется обращаться к памяти в несколько приемов. В лучшем случае система потратит на доступ к невыровненному значению в 2 раза больше времени, чем на доступ к выровненному! Так что, если Вы хотите оптимизировать работу своей программы, позаботьтесь о правильном выравнивании данных.

        Рассмотрим, как справляется с выравниванием данных процессор типа x86. Такой процессор в регистре EFLAGS содержит специальный битовый флаг, называемый флагом AC (alignment check). По умолчанию, при первой подаче питания на процессор он сброшен. Когда этот флаг равен 0, процессор автоматически выполняет инструкции, необходимые для успешного доступа к невыровненным данным. Однако, если этот флаг установлен (равен 1), то при каждой попытке доступа к невыровненным данным процессор инициирует прерывание INT 17h. Версия Windows 2000 для процессоров типа x86 и Windows 98 никогда не изменяют этот битовый флаг процессора. Поэтому в программе, работающей на процессоре типа x86, исключения, связанные с попыткой доступа к невыровненным данным, никогда не возникают.

        Теперь обратим внимание на процессор Alpha. Он не умеет оперировать с невыровненными данными. Когда происходит попытка доступа к таким данным, этот процессор уведомляет операционную систему. Далее Windows 2000 решает, что делать — генерировать соответствующее исключение или самой устранить возникшую проблему, выдав процессору дополнительные инструкции. По умолчанию Windows 2000, установленная на компьютере с процессором Alpha, сама исправляет все ошибки обращения к невыровненным данным. Однако Вы можете изменить ее поведение. При загрузке Windows 2000 проверяет раздел реестра:

        Цитата

        HKEY_LOCAL_MACHINE\CurrentControlSet\Control\Session Manager


        В этом разделе может присутствовать параметр EnableAlignmentFaultExceptions. Если его нет (что чаще всего и бывает), Windows 2000 сама исправляет ошибки, связанные с доступом к невыровненным данным. Но, если он есть, система учитывает его значение. При его нулевом значении система действует так же, как и в отсутствие этого параметра. Если же он равен 1, система не исправляет такие ошибки, а генерирует исключения. Никогда не модифицируйте этот параметр в реестре без особой необходимости, потому что иначе некоторые приложения будут вызывать исключения из-за доступа к невыровненным данным и аварийно завершаться.

        Чтобы упростить изменение этого параметра реестра, с Microsoft Visual C++ для платформы Alpha поставляется утилита AXPAlign.exe. Она используется так, как показано ниже.

        Цитата

        Alpha AXP alignment fault exception control
        Usage axpalign [option]
        Options:
        /enable to enable alignment fault exceptions
        /disable to disable alignment fault exceptions
        /show to display the current alignment exception setting
        Enable alignment fault exceptions


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

        Заметьте, что SetErrorMode(SEM_NOALIGNMENTFAULTEXCEPT) позволяет подавить генерацию таких исключений даже в этом режиме.

        Disable alignment fault exceptions

        Этот режим действует по умолчанию в Windows NT for Alpha AXP версий 3.1 и 3.5. Операционная система сама исправляет любые ошибки связанные с доступом к невыровненным данным (если таковые ошибки возникают) и приложения или отладчики их не замечают. Если программа часто обращается к невыровненным данным производительность системы может заметно снизиться. Для наблюдения за частотой появления таких ошибок можно использовать Perfmon или wperf.

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

        Но, даже не пользуясь утилитой AXPAlign, Вы все равно можете заставить систему молча исправлять ошибки обращения к невыровненным данным во всех потоках Вашего процесса. Для этого один из потоков должен вызвать функцию SetErrorMode:

        ExpandedWrap disabled
          UINT SetErrorMode(UINT fuErrorMode);


        В данном случае Вам нужен флаг SEM_NOALIGNMENTFAULTEXCEPT. Когда он установлен, система автоматически исправляет ошибки обращения к невыровненным данным, а когда он сброшен, система вместо этого генерирует соответствующие исключения. Заметьте, что изменение этого флага влияет на потоки только того процесса, из которого была вызвана функция SetErrorMode. Иначе говоря, его модификация не отражается на потоках других процессов. Также учтите, что любые флаги режимов обработки ошибок наследуются всеми дочерними процессами. Поэтому перед вызовом функции CreateProcess Вам может понадобиться временно сбросить этот флаг.

        SetErrorMode можно вызывать с флагом SEM_NOALIGNMENTFAULTEXCEPT независимо от того, на какой платформе выполняется Ваше приложение. Но результаты ее вызова не всегда одинаковы. На платформе x86 сбросить этот флаг просто нельзя, а на платформе Alpha его разрешается сбросить, только если параметр EnableAlignmentFaultExceptions в реестре равен 1.

        Для наблюдения за частотой возникновения ошибок, связанных с доступом к невыровненным данным, в Windows 2000 можно использовать Performance Monitor, подключаемый к MMC. На следующей иллюстрации показано диалоговое окно Add Counters, которое позволяет добавить нужный показатель в Performance Monitor.



        Этот показатель сообщает, сколько раз в секунду процессор уведомляет операционную систему о доступе к невыровненным данным. На компьютере с процессором типа x86 он всегда равен 0. Это связано с тем, что такой процессор сам справляется с проблемами обращения к невыровненным данным и не уведомляет об этом операционную систему. А поскольку он обходится без помощи со стороны операционной системы, падение производительности при частом доступе к невыровненным данным не столь значительно, как на процессорах, требующих с той же целью участия операционной системы.

        Как видите, простого вызова SetErrorMode вполне достаточно для того, чтобы Ваше приложение работало корректно. Но это решение явно не самое эффективное. Так, в AlphaArchitectureReferenceManual, опубликованном Digital Press, утверждается, что системный код, автоматически устраняющий ошибки обращения к невыровненным данным, может снизить быстродействие в 100 раз! Издержки слишком велики. К счастью, есть более эффективное решение этой проблемы.

        Компилятор Microsoft С/С++ для процессоров Alpha поддерживает ключевое слово __unaligned. Этот модификатор используется так же, как const или volatile, но применим лишь для переменных-указателей. Когда Вы обращаетесь к данным через невыровненный указатель (unaligned pointer), компилятор генерирует код, исходя из того, что данные скорее всего не выровнены, и вставляет дополнительные машинные инструкции, необходимые для доступа к таким данным. Ниже показан тот же фрагмент кода, что и в начале раздела, но с использованием ключевого слова __unaligned.

        ExpandedWrap disabled
          VOID SomeFunc(PVOID pvDataBuffer)
          {
              // первый байт в буфере содержит значение типа BYTE
              char с = * (PBYTE} pvDataBuffer;
           
              // увеличиваем указатель для перехода за этот байт
              pvDataBuffer = (PVOID)((PBYTE) pvDataBuffer + 1);
           
              // байты 2-5 в буфере содержат значение типа DWORD
              DWORD dw = * (__unaligned DWORD *) pvDataBuffer;
           
              // Предыдущая строка заставит компилятор сгенерировать дополнительные
              // машинные инструкции, которые позволят считать значение типа DWORD
              // в несколько приемов. При этом исключение из-за попытки доступа
              // к невыровненным данным не возникнет.
          }


        При компиляции следующей строки на процессоре Alpha, генерируется 7 машинных инструкций:

        ExpandedWrap disabled
          DWORD dw = * (__unaligned DWORD *) pvDataBuffer;
          Но если я уберу ключевое слово __unaligned, то получу всего 3 машинные инструкции. Как видите, модификатор __unaligned на процессорах Alpha приводит к увеличению числа генерируемых машинных инструкций более чем в 2 раза. Но инструкции, добавляемые компилятором, все равно намного эффективнее, чем перехват процессором попыток доступа к невыровненным данным и исправление таких ошибок операционной системой.
           
          И последнее. Ключевое слово __unaligned на процессорах типа x86 компилятором Visual С/С++ не поддерживается. На этих процессорах оно просто не нужно. Но это означает, что версия компилятора для процессоров x86, встретив в исходном коде ключевое слово __unaligned, сообщит об ошибке. Поэтому, если Вы хотите создать единую базу исходного кода приложения для обеих процессорных платформ, используйте вместо __unaligned макрос UNALIGNED. Он определен в файле WinNT.h так:
           
          #if defined(_M_MRX000) || defined(_M_ALPHA) || defined(_M_IA64)
          #  define UNALIGNED __unaligned
          #  if defined(_WIN64)
          #    define UNALIGNED64 __unaligned
          #  else
          #    define UNALIGNED64
          #  endif
          #else
          #  define UNALIGNED
          #  define UNALIGNED64
          #endif
          Добрый день!

          Цитата Бобёр @
          Этот мусор может сыграть злую шутку, когда, например, 2 разные программы, собранные с различными
          опциями компилятора "structure alignment" могут по разному интерпретировать одни и те же двоичные
          данные, что может привести к довольно страшным последствиям. В следствии этого рекомендуется
          перед сохранением и чтением структур выставлять опцию компилятора #pragma pack (push, 1) а после
          чтения - #pragma pack (pop).


          немного не понятно в этом месте. Не могли бы Вы привести пример, хотя бы схематичный?
          0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
          0 пользователей:


          Рейтинг@Mail.ru
          [ Script Execution time: 0,1019 ]   [ 15 queries used ]   [ Generated: 15.04.21, 10:40 GMT ]