Welcome to TechNet Blogs Sign in | Join | Help

Почему "4" Тоже Магическое Число

К тому, что семь – число магическое, все вроде бы привыкли. А почему четыре?

А потому, что согласно “__fastcall” calling convention, первые четыре аргумента функции передаются через регистры, а не через stack. То есть вызов функции

int __fastcall fooArg4(int a1, int a2, int a3, int a4);

должен работать *непропорционально* быстрее, чем

int __fastcall fooArg5(int a1, int a2, int a3, int a4, int a5);

А как же тогда быть с утверждением о том, что только первые 2 аргумента передаются через регистры ECX и EDX?

Оставим пока без обсуждения "родной до боли" x86, где современные процессоры делают много интересных трюков (на уровне микро архитектуры) для преодоления проблемы малого количества доступных программе регистров, и архитектуру IA64 “Itanium”, где регистров и так хватает для передачи большого числа аргументов.

Согласно MSDN, число 4 действительно является "магическим" для архитектур x64 и ARM. То есть добавление пятого аргумента функции по идее должно приводить к *нелинейному* увеличению времени вызова.

Но что бы ни написали в документации, часто хочется убедиться и знать наверняка. То есть, попробовать и измерить. Давайте попробуем:

int __fastcall fooArg4(int a1, int a2, int a3, int a4)
{
    return a1 + a2 + a3 + a4;
}

int __fastcall fooArg5(int a1, int a2, int a3, int a4, int a5)
{
    return a1 + a2 + a3 + a4 + a5;
}

int a1 = 1; int a2 = 2; int a3 = 3; int a4 = 4; int a5 = 5;

extern "C" void ExecuteMainBlock()
{
    int sum  = fooArg4(a1, a2, a3, a4);
        sum += fooArg5(a1, a2, a3, a4, a5);

    printf("sum: %d\n", sum);
}

int APIENTRY _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
    ExecuteMainBlock();
    return 0;
}

А теперь внимание, Вопрос: Как точно измерить количество циклов процессора (CPU core cycles), затраченных на выполнение соответствующих вызовов?

Опережая неотвратимую дискуссию о том, как правильно использовать и интерпретировать показания различных таймеров высокого разрешения (High Resolution Timers), позвольте сразу предложить правильный ответ: А НИКАК не измерить!

Потому что измерять скорость исполнения Debug Build с выключенной оптимизацией очевидно не имеет никакого смысла, а измерять Release Build этого примера c полной оптимизацией тоже не имеет смысла, что совсем не так уж очевидно. Почему? А потому, что оптимизирующий кодогенератор выдаст для предложенного примера следующий результат:

|WinMain| PROC
        str   lr, [sp, #-4]!

        ldr   r0, [pc, #0xC]
        mov   r1, #0x19
        bl    printf

        mov   r0, #0
        ldr   pc, [sp], #4
        ENDP

Что в переводе с "языка" ARM означает

int APIENTRY _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
    printf("sum: %d\n", 25);
    return 0;
}

Так что же делать? Как нам изменить предложенный пример, чтобы заставить компилятор построить оптимизированный код для вызова интересующих нас функций?

Предложения в студию, please! Если я опять не напортачил с конфигурированием блоггера, то канал комментариев должен быть открыт для всех.

Подсказка: предполагается, что обсуждение этого вопроса должно плавно перейти в обсуждение разных "интересностей" из Link Time Code Generation (LTCG).

Кому нужен Right Margin

Любой профессиональный текстовый редактор умеет показывать правую границу "полей" (Right Margin). Встроенный редактор Visual Studio 2005 не исключение, только по различным причинам эта функция не выведена в Menu / Toolbars. Включается очень просто через ключ в Registry:

REGEDIT4
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\8.0\Text Editor]
"Guides"="RGB(192,192,192) 98"

Подробнее эта  функция описана здесь. А нашёл и показал мне эту ссылку Matt Neerincx, потому что как-то раз у нас заходил разговор о том, почему я так внимательно слежу за длиной строк в моём коде.

Ну и почему же, интересно знать? Понятное дело, в 198-лохматом году было важно попадать в 80 колонок на терминале и в 132 колонки на АЦПУ (Алфавитно-Цифровое Печатающее Устройство; тогда ещё не было в обиходе слова "принтер"). Но сегодня, когда можно настроить размер шрифтов для печати и растянуть окно редактора на весь 21-дюймовый монитор, кого это волнует?

А вот пример:

На первый взгляд, ничего плохого в этом вымышленном примере не наблюдается. За исключением того, что результат работы этой функции устойчиво отличается от аккуратно посчитанного по формуле ровно на единицу. Без единой начальной идеи о причинах происходящего, поиск проблемы может завести очень далеко, вплоть до долгих и мучительных поисков ошибки в компиляторе. И зачастую, когда "горят сроки" и начальство постоянно "дышит в затылок", именно так и происходит – копая все глубже, мы утрачиваем способность видеть то, что лежит на поверхности:

Конечно, этот пример "высосан из пальца", и возможность наступить на такие грабли в современных условиях ничтожно мала. Но суть от этого не меняется: инспектировать код с очень длинными строками сложнее, быстрее "замыливается глаз" и больше шансов пропустить ошибку где-нибудь в 234-й колонке.

Стандартная ширина текста в 80 колонок – это, скорее всего, анахронизм. Сегодня мы имеем полную возможность "смотреть на вещи шире". Например, удобная цифра - 98, она хорошо подходит к любому из общепринятых размеров табуляции - 2,3,4,8.

В конце концов, цифра не важна, важно чтобы она была. Если убрать грабли из-под ног, аккуратно прислонив к стенке в углу за шкафом, то можно бегать по коридору во время очередного аврала без риска на эти грабли наступить.

 

О пользе Warning Level /W4

Современный компилятор Microsoft С/C++ способен предупреждать о большом количестве потенциальных проблем в исходном коде, что часто бывает слишком назойливо при компиляции миллионов строк старого кода. Поэтому по умолчанию используется уровень предупреждений /W3.

 

С другой стороны, это просто неразумно не использовать такую помощь со стороны компилятора, если мы создаем новый код на C++ и не слишком скованы вопросами обратной совместимости (backward compatibility). И все же уровень предупреждений /W4 в "чистом виде" порождает слишком много предупреждений, которые хочется видеть только "когда спросишь". Иначе за большим потоком "назойливых" предупреждений легко проглядеть важное.


Поэтому стандартная конфигурация во всех проектах, над которыми я работал и работаю такова:

 

- Warning level /W4 обязателен;

- Флаг /WX делает отсутствие предупреждений обязательным условием успешной компиляции;

- Используется выработанный годами список предупреждений, которые полезно отключить, чтобы сделать уровень /W4 "разумно строгим":

 

#if !defined( _ALL_WARNINGS )
    #pragma warning(disable: 4061)   // not all enum values present in switch statement
    #pragma warning(disable: 4068)   // unknown pragma
    #pragma warning(disable: 4127)   // constant expression
    #pragma warning(disable: 4201)   // nameless structs are used
    #pragma warning(disable: 4214)   // nonint packed fields are used
    #pragma warning(disable: 4275)   // nonexport class is a base for export one
    #pragma warning(disable: 4310)   // cast truncates constant value
    #pragma warning(disable: 4511)   // copy constructor could not be generated
    #pragma warning(disable: 4512)   // assignment operator could not be generated
    #pragma warning(disable: 4705)   // statement has no effect in optimized code
    #pragma warning(disable: 4725)   // FDIV Pentium(r) known issue

 

    #if defined( _DEBUG )
      #pragma warning(disable: 4706) // assignment within conditional expression
    #else
      #pragma warning(disable: 4702) // unreachable code caused by optimizations
      #pragma warning(disable: 4791) // loss of debugging info in retail version
    #endif

 

    #if !defined( _SHOW_INLINE )
      #pragma warning(disable: 4505) // unreferenced static function removed
      #pragma warning(disable: 4514) // unreferenced inline function removed
      #pragma warning(disable: 4710) // inline function not expanded
    #endif

 

    #if !defined( _SHOW_DEPRECATED )
      #pragma warning(disable: 4996) // 'function' was declared deprecated
    #endif
#endif

 

Важно:


- Все #pragma warning(disable) должны быть сосредоточены в одном, хорошо известном месте. Это помогает четко отслеживать конфигурацию проекта, например при такой обязательной процедуре как инспекция кода (code inspection). Поэтому приведенный выше фрагмент полезно включить в header file самого низкого уровня иерархии, что-нибудь типа BaseAPI.h или Common.h. Тогда все единицы компиляции *.cpp будут обрабатываться одинаково.


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

 

- Чем более предсказуемы имена управляющих символов, тем лучше. Например, я использую _ALL_WARNINGS, а не какой-нибудь SHOW_DISABLED_WARNINGS по одной простой причине: глобальный поиск по дереву проекта покажет также и _AFX_ALL_WARNINGS, и _ATL_ALL_WARNINGS. А если все управляющие символы условной компиляции явно перечислены в каком-нибудь ReadMeFirst.txt из корневого каталога проекта, тогда уж вообще хорошо.


  

"Литературное" Вступление

Первая и единственная "литературная" заметка. Информация по существу начинается с раздела "Для Затравки".

 

О себе

В ответ на вопросы о моей карьере (Как тебя так угораздило? :) я обычно отшучиваюсь:  обмишурился, дескать, в 1978 году, когда, будучи студентом МАИ, написал свою первую программу (на перфокартах, разумеется) и с тех пор "подсел на иглу". Нет большего пространства для творчества, чем цифровой мир. Где все ограничивается только пределами нашей фантазии, полетом мысли. Где не нужно тратить силы и время для добывания холстов и красок, как художник, или бегать по спекулянтам в поисках нужной микросхемы, как радиолюбитель, или разгружать по ночам вагоны, чтобы купить "фирменные" струны для самодельных гитар, как музыкант известного на весь микрорайон вокально-инсрументальный ансамбля (в 70-х так было положено называть свою рок-группу :)

 

Позвольте, странный контекст получается. Среди таких возвышенных понятий как Искусство, Музыка, Полет Мысли, Творчество, слово Ремесленник похоже на оскорбление. За что ж я так себя, любимого?

 

О Промышленном Программировании

  • Вы программист на Майкрософте? Ооо! Стало быть, большой математик! Мехмат? Физтех?
  • Да нет, "от станка" мы, знаете ли. С математикой у нас так себе. То есть какого-нибудь революционного алгоритма видео компрессии от нас не дождутся.
  • Ха, так что же Вы тогда делаете на Майкрософте?
  • Дело в том, что "полет мысли", творчество, и.т.п. (см. первый абзац) занимает в промышленном производстве коммерческих программных продуктов очень небольшое (не по важности конечно, а по объему вложенных средств и времени) место. Все остальное – это технологические процессы, профессиональные навыки, огромное количество мелких деталей. Одним словом, РЕМЕСЛО.

Об этих Заметках

 

Российская математическая школа сильна, это всем известно. И Майкрософт действительно с удовольствием принимает на работу выпускников математических кафедр из России. Но с вопросами технологии промышленного производства им приходится осваиваться прямо "на месте", и это не всегда происходит безболезненно.

 

В этих заметках я собираюсь, в абсолютно неструктурированной форме, делиться теми маленькими кусочками знания и 20+ летнего опыта работы в индустрии, которые мне и позволяют делать то, что я умею делать – участвовать в промышленном производстве (Making Stuff That Works). Поэтому я пишу слово Ремесленник с большой буквы.

 

О Читателях этих Заметок

 

Кому это может быть интересно? TechNet.com ориентирован на IT-Pro, в отличии от MSDN.com что для программистов-разработчиков. Но мы-то с Вами знаем, что в России IT специалист, который вообще не умеет программировать, еще долго будет нонсенс. Поэтому есть надежда, что "маленькие хитрости" из различных областей системного программирования могут быть не только интересны, но и практически полезны тем, кто понимает роль маленьких деталей и цену маленьких оплошностей.

 

Как положено, Об Ответственности (Disclaimer)

  • Всё опубликованное в этих заметках является личным мнением автора. Иначе говоря, я не имею права и не говорю от лица Майкрософт.
  • Вся информация в этих заметках предоставляется на правах КАК-ЕСТЬ (AS-IS), то-бишь: хотя я пишу то, что знаю и не пишу того, что "слышал, но забыл где", это не означает, что никогда и нигде не может быть никаких ошибок. Если Вы используете предоставленную информацию, то под свою ответственность.

Предположения и Соглашения

  • Предполагается, что читатели этих заметок уверенно владеют C/C++,  программированием для Microsoft Windows (разумеется), и (конечно же) без ума от C#.
  • Про русско-английскую смесь. Конечно же, я смеюсь вместе со всеми над Задорновским "Вам чиз наслайсать или целым писом", но также хорошо помню свои сложности, когда впервые увидел полностью русифицированный Windows-98. Поэтому многие технические термины будут использоваться в англоязычной транскрипции.
  • О вреде и "пользе" курения. В стародавние времена, в советских НИИ, курилка не только наносила вред здоровью, но и была местом естественного обмена технической информацией: "Ух, я только что чего нарыл..!!" Поэтому вместо распространенной в Америке формы предоставления информации "Did you know that…" (А Вы знали что...) мне больше нравится форма "I didnt know that.." (я не знал, что..)

Для Затравки

(чтобы представить, как эти заметки предположительно должны выглядеть)

 

Компилятор C++ из Visual Studio 2005 предоставляет вложенному классу прямой доступ к protected and private members класса объемлющего. Поэтому вложенный класс больше не обязательно объявлять как friend.

class Outer

{

   void   usedByHelperOnly();

   void   onHelperReleased();

 

 public:

   Outer()      { .. }

   ~Outer()     { .. }

 

   class Helper

   {

       Outer&  _rHost;

 

     public:

       Helper(Outer& host) : _rHost(host)            { ; }

       ~Helper()            { _rHost.onHelperReleased(); }

 

       void    DoTheJob()   { _rHost.usedByHelperOnly(); }

   };

 

   friend class Helper;  // Not required in Visual Studio 2005

 

}; // Outer

 

Есть мнение :) что интерфейс IMalloc, полученный из CoGetMalloc(), "тяжелее" чем "чистый" Win32 HeapAlloc. На самом деле реальной разницы в скорости нет.

IMalloc* pMalloc = NULL;

HRESULT  hr      = CoGetMalloc(MEMCTX_TASK, &pMalloc);

 

if( FAILED(hr) ){ .. }

 

После получения интерфейса,

pData = pMalloc->Alloc(123456);

это тоже самое что и

pData = HeapAlloc(GetProcessHeap(), 0, 123456);

 

 

 
Page view tracker