Городские легенды о медленных вызовах виртуальных функций

    Традиционно компиляторы реализуют вызовы виртуальных функций через двойную косвенную адресацию — если класс содержит хотя бы одну виртуальную функцию, то в начале каждого объекта этого класса хранится адрес таблицы виртуальных функций. Если компилятор не знает конкретный тип объекта, на который указывает указатель, то для вызова виртуальной функции нужно сначала взять указатель на объект, прочитать адрес начала таблицы, затем по номеру метода прочитать адрес, где хранится реализация функции, затем вызвать функцию.

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

    В тексте выше ключевое слово «если». Что, если компилятор знает, какую функцию на самом деле надо вызывать?

    В Стандарте (далее ссылки на Стандарт C++03) ничего не сказано про таблицы виртуальных методов. Вместо этого в 5.2.2/1 ([expr.call], «вызов функции») сказано, что если в программе содержится вызов виртуальной функции, то должна быть вызвана соответствующая функция, выбранная по правилам из 10.3/2 ([class.virtual], «виртуальные функции»), а там сказано, что TL;DR; должна быть выбрана функция из самого производного класса, в котором функция определена или переопределена.

    Соответственно, если компилятор может, разобрав код, выяснить точный тип объекта, он не обязан использовать позднее связывание – и не важно, вызывается метод у конкретного объекта, по ссылке или по указателю на объект.

    От бессмысленных рассуждений перейдем к коду, который будем пробовать на gcc.godbolt.org

    Нам понадобятся вот эти два класса:

    class Base {
    public:
        virtual ~Base() {}
        virtual int Magic() {
            return 9000;
        }
    };
    
    class Derived : public Base {
    public:
        virtual int Magic() {
            return 100500;
        }
    };


    Для начала такой код:

    int main()
    {
        Derived derived;
        return derived.Magic();
    }
    

    clang 3.4.1 с -O2 отвечает на это так:
    main:    # @main
        movl     $100500, %eax    # imm = 0x18894
        ret
    

    Нетрудно видеть, что машинный код соответствует программе, содержащей только return 100500; Это не особенно интересно — ведь здесь нет указателей и ссылок.

    Ладно, медленно помешивая, добавляем указатели и ссылки:

    int magic( Base& object )
    {
        return object.Magic();
    }
    
    int main()
    {
        Base* base = new Derived();
        int result = magic( *base );
        delete base;
        return result;
    }

    clang 3.4.1 с -O2 отвечает на это так:
    magic(Base&):    # @magic(Base&)
        movq    (%rdi), %rax
        jmpq    *(%rax)    # TAILCALL
    
    main:    # @main
        movl     $100500, %eax    # imm = 0x18894
        ret
    


    ОХ ЩИ ОШИБКА В КОМПИЛЯТОРЕ Нет, с компилятором все в порядке, но агрессивность оптимизации отрицать бессмысленно. Снова return 100500;

    Для сравнения, gcc 4.9.0 с -O2:

    main:
        subq     $8, %rsp
        movl     $8, %edi
        call    operator new(unsigned long)
        movq    vtable for Derived+16, (%rax)
        movq    %rax, %rdi
        call    Derived::~Derived()
        movl     $100500, %eax
        addq     $8, %rsp
        ret
    


    call Derived::~Derived() – из-за виртуального деструктора, gcc в таких случаях ставит вызов ::operator delete() внутрь деструктора:

    Derived::~Derived():
        jmp    operator delete(void*)
    

    хотя мог бы и по месту подставить. Вот так:
        movq    %rax, %rdi
        call    operator delete(void*)
    

    Мог бы, но не стал. В то же время тело метода Derived::Magic() подставлено в место вызова и оптимизировано вместе с окружающим кодом.

    Небольшое отступление… Если вы любите пространно рассуждать о том, насколько хорошо компилятор в принципе может оптимизировать код, пример выше для вас. И вызов Derived::Magic(), и удаление объекта компилятор мог оптимизировать одинаково успешно, но один из них он оптимизировал, а второй – нет. Отступление закончено.

    Для сравнения, gcc 4.9.0 с -O1

    magic(Base&):
        subq    $8, %rsp
        movq    (%rdi), %rax
        call    *(%rax)
        addq    $8, %rsp
        ret
    main:
        pushq   %rbp
        pushq   %rbx
        subq    $8, %rsp
        movl    $8, %edi
        call     operator new(unsigned long)
        movq    %rax, %rbx
        movq    vtable for Derived+16, (%rax)
        movq    %rax, %rdi
        call    magic(Base&)
        movl    %eax, %ebp
        testq    %rbx, %rbx
        je    .L12
        movq    (%rbx), %rax
        movq    %rbx, %rdi
        call    *16(%rax)
    .L12:
        movl    %ebp, %eax
        addq    $8, %rsp
        popq    %rbx
        popq    %rbp
        ret
    


    Вот, может ведь, если хорошо попросить. В этом коде «все в порядке» — куча доступов к памяти и вызов метода инструкцией call с косвенной адресацией (call *16(%rax)).

    Впрочем, примеры успеха с -O2 выглядят надуманными – весь код находится в одной единице трансляции, а это существенно упрощает оптимизацию.

    На помощь спешит LTO (или как там называется оптимизация нескольких единиц трансляции в вашем компиляторе).

    Делим код на несколько единиц трансляции…

    //Classes.h
    class Base {
    public:
        virtual int Magic();
        virtual ~Base();
    };
    
    class Derived : public Base {
    public:
        virtual int Magic();
    };
    
    //Classes.cpp
    #include <Classes.h>
    #include <stdio.h>
    
    Base::~Base()
    {
    }
    
    int Base::Magic()
    {
        return 9000;
    }
    
    int Derived::Magic()
    {
        return 100500;
    }
    
    //main.cpp
    #include <Classes.h>
    int magic( Base& object )
    {
        return object.Magic();
    }
    
    int main()
    {
        Base* base = new Derived();
        int result = magic( *base );
        delete base;
        return result;
    }
    


    Здесь и далее будем использовать MinGW с gcc 4.9.0

    g++ -flto -g -O3 main.cpp Classes.cpp
    objdump -d -M intel -S --no-show-raw-insn a.exe >a.txt
    


    int main()
    {
        402830:	push   ebp
        402831:	mov    ebp,esp
        402833:	and    esp,0xfffffff0
        402836:	sub    esp,0x10
        402839:	call   402050 <___main>
        Base* base = new Derived();
        40283e:	mov    DWORD PTR [esp],0x4
        вызов ::operator new()
        402845:	call   4015d8 <__Znwj>
        запись указателя на vtable
        40284a:	mov    DWORD PTR [eax],0x404058
        int result = magic( *base );
        delete base;
        402850:	mov    ecx,eax
        402852:	call   4015c0 <__ZN7DerivedD0Ev>
        return result;
    }
        на место возвращаемого значения записывается константа 
        402857:	mov    eax,0x18894
        40285c:	leave  
        40285d:	ret    
    

    Здесь нас интересует инструкция mov eax, 0x18894 (100500 в шестнадцатеричной записи) — снова компилятор выбрал нужную функцию, подставил ее тело в место вызова и оптимизировал окружающий код.

    Слишком просто, поэтому добавляем фабрику (Derived и Base те же)…
    //Factory.h
    #include <Classes.h>
    
    class Factory {
    public:
        static Base* CreateInstance();
    };
    
    //Factory.cpp
    #include <Factory.h>
    
    Base* Factory::CreateInstance()
    {
        return new Derived();
    }
    
    //main.cpp
    #include <Factory.h>
    
    int magic( Base& object )
    {
        return object.Magic();
    }
    
    int main()
    {
        Base* base = Factory::CreateInstance();
        int result = magic( *base );
        delete base;
        return result;
    }
    

    Компилируем, дизассемблируем… Исходно результат выглядит не очень понятно – из-за агрессивной оптимизации машинный код и исходный код оказались сопоставлены не самым удобным для чтения образом, ниже машинный код оставлен как есть, а часть строк исходного кода размещена максимально близко к соответствующему машинному коду.
    int main()
    {
        402830:	push   ebp
        402831:	mov    ebp,esp
        402833:	push   esi
        402834:	push   ebx
        402835:	and    esp,0xfffffff0
        402838:	sub    esp,0x10
        40283b:	call   402050 <___main>
        return new Derived();
        402840:	mov    DWORD PTR [esp],0x4
        вызов ::operator new()
        402847:	call   4015d8 <__Znwj>
        40284c:	mov    ebx,eax
    
    int magic( Base& object )
    {
        return object.Magic();
        40284e:	mov    ecx,eax
        запись указателя на vtable
        402850:	mov    DWORD PTR [eax],0x404058
        прямой вызов Derived::Magic()
        402856:	call   401580 <__ZN7Derived5MagicEv>
    
    int main()
    {
        delete base;
        40285b:	mov    ecx,ebx
        40285d:	mov    esi,eax
        40285f:	call   4015b0 <__ZN7DerivedD0Ev>
        return result;
    
        402864:	lea    esp,[ebp-0x8]
        402867:	mov    eax,esi
        402869:	pop    ebx
        40286a:	pop    esi
        40286b:	pop    ebp
        40286c:	ret 
     (дальше пропущено)
    


    Здесь нас интересует строка
        402856:	call   401580 <__ZN7Derived5MagicEv>
    


    Это прямой вызов Derived::Magic():
      00401580 <__ZN7Derived5MagicEv>:
    
    int Derived::Magic()
    {
        return 100500;
    }
        401580:	mov    eax,0x18894
        401585:	ret 
    


    Компилятор правильно определил, какую функцию нужно вызвать, но не стал подставлять тело функции в место вызова.

    Параметризуем фабрику (Base и Derived те же)…
    //Factory.h
    #include <Classes.h>
    enum ClassType {
        BaseType,
        DerivedType
    };
    
    class Factory {
    public:
        static Base* CreateInstance(ClassType classType);
    };
    
    //Factory.cpp
    #include <Factory.h>
    
    Base* Factory::CreateInstance(ClassType classType)
    {
        switch( classType ) {
            case BaseType:
               return new Base();
            case DerivedType:
               return new Derived();
        }
    }
    
    //main.cpp
    #include <Factory.h>
    int magic( Base& object )
    {
        return object.Magic();
    }
    
    int main()
    {
        Base* base = Factory::CreateInstance(DerivedType);
        int result = magic( *base );
        delete base;
        return result;
    }
    

    Получаем… тот же код, что и в предыдущей попытке.

    Теперь party hard будем вычислять параметр фабрики при работе программы…
    #include <Factory.h>
    #include <cstdlib>
    
    int magic( Base& object )
    {
        return object.Magic();
    }
    
    int main()
    {
        Base* base = Factory::CreateInstance(rand() ? BaseType : DerivedType);
        int result = magic( *base );
        delete base;
        return result;
    }
    

    Получаем… (результат опять выглядит не очень понятно)
    int main()
    {
        402830:	push   ebp
        402831:	mov    ebp,esp
        402833:	push   esi
        402834:	push   ebx
        402835:	and    esp,0xfffffff0
        402838:	sub    esp,0x10
        40283b:	call   402050 <___main>
        Base* base = Factory::CreateInstance(rand() ? BaseType : DerivedType);
        вызов rand() 
        402840:	call   4027c8 <_rand>
    
    Base* Factory::CreateInstance(ClassType classType)
    {
        switch( classType ) {
        проверка из объединенных вместе тернарного оператора и switch
        402845:	test   eax,eax
        402847:	mov    DWORD PTR [esp],0x4
        ветвление
        40284e:	jne    402875 <_main+0x45>
        если rand() вернула не ноль, происходит переход вперед на адрес 402875
        если rand() вернула ноль, перехода нет и ...
        case DerivedType:
            return new Derived();
        вызов ::operator new()  
        402850:	call   4015d8 <__Znwj>
        запись указателя на vtable класса Derived
        402855:	mov    DWORD PTR [eax],0x404070
        40285b:	mov    ebx,eax
    
    int magic( Base& object )
    {
        return object.Magic();
        здесь происходит слияние двух ветвей -
        управление либо "проваливается" сюда, либо безусловно
        приходит из ветви, начинающейся с адреса 402875 (rand() != 0)
        40285d:	mov    eax,DWORD PTR [ebx]
        40285f:	mov    ecx,ebx
        косвенный вызов Magic()
        402861:	call   DWORD PTR [eax]
        402863:	mov    esi,eax
    
    int main()
    {
        delete base;
        402865:	mov    eax,DWORD PTR [ebx]
        402867:	mov    ecx,ebx
        косвенный вызов удаления объекта
        402869:	call   DWORD PTR [eax+0x8]
        return result;
    }
        40286c:	lea    esp,[ebp-0x8]
        40286f:	mov    eax,esi
        402871:	pop    ebx
        402872:	pop    esi
        402873:	pop    ebp
        402874:	ret    
    
    Base* Factory::CreateInstance(ClassType classType)
    {
        switch( classType ) {
            case BaseType:
               return new Base();
        сюда управление приходит при ветвлении по адресу 40284e
        если rand() != 0
        вызов ::operator new()
        402875:	call   4015d8 <__Znwj>
        запись указателя на vtable класса Base
        40287a:	mov    DWORD PTR [eax],0x404058
        402880:	mov    ebx,eax
        окончание ветви и безусловный переход в точку слияния
        402882:	jmp    40285d <_main+0x2d>
    


    Довольно любопытный результат. Код метода фабрики полностью подставлен по месту. В зависимости от результата вызова функции rand() прямо внутри main() выполняется ветвление и создание экземпляров соответствующих классов. Компилятор мог бы дальше поставить и прямые вызовы в каждой из ветвей, но не справился с оптимизацией и скатился в два косвенных вызова – один для вызова метода Magic() с поздним связыванием, второй – для удаления объекта, тоже с поздним связыванием.

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

    Дмитрий Мещеряков,
    департамент продуктов для разработчиков
    ABBYY
    114.04
    Решения для интеллектуальной обработки информации
    Share post

    Comments 38

      –7
      А значит, черт лысый не знает, что творится в наших программах. Хотелось бы точно знать, когда вызов с поздним связыванием, а когда на стадии компиляции все уже известно. Но почтенный возраст сказывается на С++…
      Jon Blow пилит язык, где позднее связывание делается пока вручную, а в перспективе макросом или в этом роде. Выглядит многообещающе.
      А в Rust можно достаточно просто определить, когда происходит связывание. Если используется параметрический полиморфизм — раннее, ad hoc — позднее связывание. Ну или могли уже это изменить — летом так было :)
        +10
        Простите. а зачем вам знать, поздние связывание или на стадии компиляции все известно?
          +3
          Два лишних указателя не всегда проходят бесследно. В играх, особенно под мобильные платформы, при оптимизации иногда приходится жертвовать уровнем абстракции ради производительности, и знание, когда именно отказ от абстракции поможет производительность улучшить, дорогого стоит, потому что пробовать перебирать миллионы вариантов и делать для них бенчмарки может быть слишком трудозатратным.
          Знать, где точно нет обращения к vtable, сужает круг возможных направлений уродования кода.
            +4
            Ну давайте подумаем. наследование с переопределением виртуальных методов позволяющий программисту не знать, что конкретно он вызывает.

            То, что компилятор разобрался — то честь ему и хвала. Но следующее движение Автора может все в корне поменять
              +4
              Тьфу, не подумал о том, что с помощью темплейтов в С++ можно точно так же, как в rust (а точнее, очевидно, наоборот — в rust как в С++) гарантировать раннее связывание. Ну, с поправкой на разницу между темплейтами и генериками в rust.
              Это — то, что меня волновало, а не предсказание оптимизации dynamic dispatch, как вы могли подумать. Извините, что взбаламутил :)
              +4
              Если вы сами знаете на этапе компиляции, что будет когда вызвано — используйте статический полиморфизм, а не динамический, он гарантирует отсутствие позднего связывания
                +4
                Вы правы, поспешил, людей вот насмешил :)
          +4
          Интересно. Для полноты картины я бы добавил, что в том подавлящем числе случаев, когда виртуальная функция вызывается косвенным вызовом через таблицу, в дело вступают оптимизации в железе.

          Такие косвенные вызовы неожиданно хорошо предсказываются.
            0
            Предсказание вызова во время работы программы — это хорошо, но процессор вынужден работать с машинным кодом, который можно было бы упростить, используя знания о том, каким конструкциям языка высокого уровня он соответствует. Вот например:
            class Derived : public Base {
            public:
                 virtual int Magic() { return 100500; }
            };
            
            void someFarAwayFunction()
            {
                Base* base = new Derived();
                std::vector<int> somethingUseful;
                //blahblahblah, fill vector
                { // this block can be removed completely
                    int uselessSum = std::accumulate( somethingUseful.begin(),
                         somethingUseful.end(), 0);
                    if( base->Magic() < 0 ) {
                         printf( "%d", uselessSum );
                    }
                }
            }
            


            Если компилятор может понять, что здесь гарантированно вызывается именно Derived::Magic(), которая возвращает положительное число, он может прийти к выводу, что вызов printf() никогда не выполняется, а из этого следует, что можно попробовать удалить и код вычисления параметров для этого вызова. В результате весь внутренний блок можно удалить, и тогда он просто не дойдет до процессора.
              0
              Здесь тоже работает предсказание ветвлений.

              Если встраивать тело функции вместо вызова возможностей для оптимизации больше — абсолютно согласен.
            –3
            Нет, с компилятором все в порядке, но агрессивность оптимизации отрицать бессмысленно.
            А меня, честно говоря, этот момент смущает. Насколько я понимаю, ассемблерный вывод идет на уровне получения объектного файла, а не готового исходника, и в этот момент компилятор ещё не может знать, что у нас не будет других единиц трансляции. Суть в том, что в любой единице трансляции мы можем подменить глобальные operator new() и operator delete() на свою реализацию, и компилятор будет обязан использовать эту реализацию для всей программы. Поэтому то, что он выкинул здесь new и delete, а, соотвественно, и вызовы этих глобальных операторов, может изменить наблюдаемое поведение.
              +3
              Переопределение new и delete будет в заголовочном файле, который препроцессор подставит в код данной единицы трансляции. Никаких «внезапно» при компиляции не бывает.
                0
                Либо я плохо объяснил, либо вы неправы. Создайте такие два файла:
                operators_replacement.cpp
                #include <iostream>
                #include <cstdlib>
                
                void* operator new(std::size_t count)
                {
                	std::cout << "Allocation\n";
                	return malloc(count);
                }
                
                void operator delete(void* ptr)
                {
                	std::cout << "Deallocation\n";
                	free(ptr);
                }
                

                main.cpp
                int main()
                {
                auto ptr = new int;
                delete ptr;
                }
                Соберите их вместе в один исполняемый файл и посмотрите на вывод. Не уверен, что этот код на 100% безопасен, т.к. он полагается на то, что std::cout не использует ::operator new() для своей инициализации (иначе произойдет запись в ещё неиницилизированный поток), но на gcc это работает. Разумеется, можно делать что-то другое (например, глобальную переменную-счетчик крутить) и тогда этой проблемы не будет.
                  0
                  Так и есть, все, что может быть вызвано извне, подставляется в виде символа, которые потом уже соберет линковщик. Именно поэтому если вы хотите применять инлайны, они должны быть доступны в момент компиляции. Хотя есть возможность инлайнить и в момент линковки.

                  Мне кажется, этот код будет на 100% безопасен, поскольку std::cout либо полагается на то, что код оператора new для библиотеки уже где-то определен и скомпонован (если библиотеки run-time), либо что будет использован доступный компоновщику код в случае static-сборки.
                    +1
                    либо что будет использован доступный компоновщику код в случае static-сборки.
                    Проблема в конкретной реализации оператора new: если std::cout вызовет его в процессе своей инициализации, то это приведет к записи в ещё неинициализированный поток. Но вопрос не в этом, а в том, почему clang выкинул вызов этих операторов из сгенерированного кода, он вроде как не мог во время генерации знать, сделаем мы такое переопределение или нет.
                      0
                      А, да, вы правы про безопасность.

                      Пытаюсь придумать адекватное объяснение и не могу придумать ничего действительно обоснованного. Такое ощущение, что компилятор за нас решил сделать функцию инлайном, и оптимизировал ее вызов вместе с созданием объекта. Интересно бы было собрать ваш код с оператором clang'ом. И я могу предполагать, что в случае с раздельными модулями, результат бы был похож на gcc.
              +5
              Это же девиртуализация :-) для помощи компилятору стоит активно использовать final, но он и сам неплохо справляется.
                0
                Это круто, конечно, но вот в местах, где критична производительность, не хочется отдавать всё на волю компилятора.
                Было бы, наверное, круто иметь какое-нибудь ключевое слово чтобы заставить компилятор принудительно вставлять конкретный вызов функции вместо vtable.
                PS
                А что будет творится с инлайнингом виртуальных функций если начнется пляска с исключениями?
                  –2
                  Код вида
                  static_cast<concrete_type*>(base_ptr)->function(args...)

                  может заставить компилятор использовать функцию конкретного типа :-)
                    +3
                    Если функция или класс не final, то нет, вдруг base_ptr указывает не на concrete_type, а на производный от него тип c переопределенной в нем function.
                      0
                      Кончено, гарантий нет, я поэтому и говорю «может». Но вообще я в уме держал что «наследоваться от конкретных типов — плохо, поэтому concrete_type final». :-)
                      Но, в некоторых случаях компилятор может и сам проследить, что concrete_type является листовым типом в иерархии.
                        +1
                        Если точно уверен, что base_ptr указывает именно на concrete_type, то
                        static_cast<concrete_type*>(base_ptr)->concrete_type::function(args...)
                          0
                          Оказывается, ниже уже ответили, не дочитал. Извиняюсь.
                        +3
                        Это делается несколько иначе. Вместо

                        static_cast<concrete_type*>(base_ptr)->function(args...)


                        нужно написать

                        (*base_ptr).concrete_type::function(args...)


                        Тогда будет вызван именно метод
                        concrete_type::function(args...)
                        , вне зависимости от того, является ли base_ptr производным классом или нет.
                          +4
                          Такой код работает только для upcasting'а, можно вызывать функцию одного из типов выше по иерархии (базовых), но не ниже.
                          Финальным корректным вариантом видится такой код:
                          static_cast<concrete_type*>(base_ptr)->concrete_type::function(args...)
                          0
                          Когда тип известен, и дело только за кастом, то лучше
                          boost::dynamic_cast<Derived>(x)
                          
                          +2
                          По-моему, это отнюдь не было бы круто. Это не нужно. Если вы знаете, что здесь по указателю Base* лежит Derived* — ну так и напишите Derived* (хотя бы приведите с помощью static_cast).
                            +3
                            Это не работает, как способ сказать компилятору «по указателю лежит Derived», это говорит компилятору лишь «по указателю лежит Derived или производный от него тип». Это работает только если Derived объявлен как final, тогда компилятор понимает, что производных типов быть не может, и девиртуализирует вызов.
                              +1
                              Хм, спасибо за замечание, вы правы.
                            +1
                            Для таких ситуаций, когда хочется полного контроля и максимальной производительности, и были придуманы шаблоны.
                            В той же STL очень мало виртуальных функций.
                            0
                            Приведенные примеры простые. Намного интереснее получается, компилятор девиртуализирует вызовы во время LTO. Такие случаи уже сложно руками оптимизировать.
                              +1
                              thanks for gcc.godbolt.org/
                                +1
                                А представляете, в Java все instance-вызовы виртуальные, вся надежда только на компиляторы. :)
                                  0
                                  Если кому-то интересно почему виртаульные методы в классическом виде сказываются на производительности можно посмотреть это длинющее видео: channel9.msdn.com/Events/GoingNative/2013/Compiler-Confidential (с 25 минуты вроде разговор заходит на эту тему).
                                    +1
                                    Слишком простые примеры.
                                    Вот если взять 100500 классов со сложной иерархией и распихать их по нескольким библиотекам, скомпилированным разными версиями компилятора вот тогда получится весело
                                    и никакая LTO не поможет.

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

                                      Способ гарантированно получить от компилятора конкретные инструкции на выходе — написать asm volatile("") и вперед с песней.

                                      Но редко кто может похвастаться умением писать на ассемблере лучше, чем это делает GCC.

                                      Второй способ — вдумчивое изучение всех опций и #pragma компилятора. А потом тесты, тесты и снова тесты.
                                      +1
                                      Спасибо за статью! Написано восхитительно, приятно читать.

                                      Вот только один момент, связанный с подсветкой синтаксиса. После слов «Здесь и далее будем использовать MinGW с gcc 4.9.0» есть кусочек кода с вызовом компилятора и objdump, который почему-то подсветился как brainfuck. В результате в Safari в режиме чтения он отображался так:
                                      ++ - - - . .
                                       - -  - ----- . >.

                                      Я вначале подумал, а не является ли это пасхалкой — даже интерпретатор поставил, но ничего вразумительного он не показал :)
                                        0
                                        Интересно получилось. При верстке был использован тег <source> без атрибута lang. При просмотре «инспектором» Огнелиса было видно, что в HTML страницы оказался элемент <source lang=«brainfuck»> Прописал lang=«bash».

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