Стоит ли оптимизировать определенные функции с помощью Ассемблера в программе на C / C ++? - PullRequest
12 голосов
/ 11 сентября 2009

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

Вопросы:

  1. Действительно ли стоит оптимизировать определенные функции с помощью Assembly в программе на C / C ++?

  2. Есть лидействительно достаточный выигрыш в производительности при оптимизации программы на C / C ++ с помощью Assembly на современных современных компиляторах?


Что я понимаю из опубликованных ответов, любой выигрыш, который можно получить, этоважно в определенных областях, таких как встроенные системы, мультимедийное программирование (графика, звук и т. д.).Кроме того, нужно быть способным (или иметь кого-то способного) выполнять работу на ассемблере лучше, чем современный компилятор.Выполнение действительно оптимизированного C / C ++ может занять меньше времени и может сделать достаточно хорошую работу.И последнее, изучение ассемблера может помочь понять внутреннюю механику программы и, в конце концов, сделать кого-то лучшим программистом.

Ответы [ 11 ]

27 голосов
/ 11 сентября 2009

Я бы сказал, что оно того не стоит. Я работаю над программным обеспечением, которое выполняет 3D-рендеринг в режиме реального времени (то есть, рендеринг без помощи графического процессора). Я широко использую встроенные функции компилятора SSE - много уродливого кода, заполненного __mm_add_ps() и друзьями, - но мне не нужно было очень долго перекодировать функцию в сборке.

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

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

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

10 голосов
/ 11 сентября 2009

Единственный возможный ответ на этот вопрос: да, если есть повышение производительности, которое является уместным и полезным.

Вопрос, который я должен предположить, на самом деле: можете ли вы добиться значительного прироста производительности, используя язык ассемблера в программе C / C ++?

Ответ - да.

Случаи, когда вы получаете значимое увеличение производительности, вероятно, уменьшились за последние 10-20 лет, поскольку библиотеки и компиляторы улучшились, но для архитектуры, такой как x86, в частности, оптимизация рук в определенных приложениях (особенно связанные с графикой) это можно сделать.

Но, как и все, не оптимизируйте, пока вам не понадобится.

Я бы сказал, что оптимизация алгоритма и написание высокоэффективного C (в частности) создадут гораздо больший выигрыш в производительности за меньшее время, чем переписывание на ассемблере в подавляющем большинстве случаев.

6 голосов
/ 11 сентября 2009

Сложность в том, можете ли вы выполнить оптимизацию лучше, чем компилятор, учитывая архитектуру современного процессора. Если вы проектируете для простого процессора (например, для встраиваемых систем), вы можете провести разумную оптимизацию, но для конвейерной архитектуры оптимизация гораздо сложнее, поскольку вам необходимо понять, как работает конвейерная обработка.

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

5 голосов
/ 11 сентября 2009

Может

Это полностью зависит от индивидуальной программы

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

Практическое правило гласит, что 90% времени выполнения приходится на 10% кода. Вы действительно хотите одно очень сильное узкое место, и не у каждой программы есть это.

Кроме того, машины теперь настолько быстры, что некоторые из низко висящих фруктов съели, так сказать, компиляторы и ядра ЦП. Например, скажем, вы пишете код лучше, чем компилятор, и сокращаете количество команд пополам. Даже тогда, если вы в конечном итоге сделаете одинаковое количество ссылок на память, и если они являются узким местом, вы не сможете выиграть.

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

Изучение ассемблирования действительно более важно как способ понять, что машина на самом деле, а не как способ победить компилятор. Но попробуйте!

4 голосов
/ 11 сентября 2009

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

Однако для автора библиотеки даже небольшие улучшения для больших усилий часто оправданы, потому что это экономит время для тысяч разработчиков и пользователей, которые в конечном итоге используют библиотеку. Тем более для авторов компиляторов. Если вы сможете получить 10% -ную выигрыш в эффективности функции библиотеки базовой системы, это может буквально сэкономить тысячелетия (или более) времени работы от батареи по всей базе пользователей.

4 голосов
/ 11 сентября 2009

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

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

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

4 голосов
/ 11 сентября 2009

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

2 голосов
/ 11 сентября 2009

определенно да!

Вот демонстрация вычисления CRC-32, который я написал на C ++, а затем оптимизировал его на ассемблере x86 с помощью Visual Studio.

InitCRC32Table () должен вызываться при запуске программы. CalcCRC32 () рассчитает CRC для данного блока памяти. Обе функции реализованы как на ассемблере, так и на C ++.

На типичном компьютере Pentium вы заметите, что функция ассемблера CalcCRC32 () на 50% быстрее, чем код C ++.

Реализация на ассемблере не MMX или SSE, а простой код x86. Компилятор никогда не создаст такой эффективный код, как созданный вручную код ассемблера.

    DWORD* panCRC32Table = NULL; // CRC-32 CCITT 0x04C11DB7

    void DoneCRCTables()
    {
        if (panCRC32Table )
        {
            delete[] panCRC32Table;
            panCRC32Table= NULL;
        }
    }

    void InitCRC32Table()
    {
        if (panCRC32Table) return;
        panCRC32Table= new DWORD[256];

        atexit(DoneCRCTables);

    /*
        for (int bx=0; bx<256; bx++)
        {
            DWORD eax= bx;
            for (int cx=8; cx>0; cx--)
                if (eax & 1)
                    eax= (eax>>1) ^ 0xEDB88320;
                else
                    eax= (eax>>1)             ;
            panCRC32Table[bx]= eax;
        }
    */
            _asm cld
            _asm mov    edi, panCRC32Table
            _asm xor    ebx, ebx
        p0: _asm mov    eax, ebx
            _asm mov    ecx, 8
        p1: _asm shr    eax, 1
            _asm jnc    p2
            _asm xor    eax, 0xEDB88320           // bit-swapped 0x04C11DB7
        p2: _asm loop   p1
            _asm stosd
            _asm inc    bl
            _asm jnz    p0
    }


/*
DWORD inline CalcCRC32(UINT nLen, const BYTE* cBuf, DWORD nInitVal= 0)
{
    DWORD crc= ~nInitVal;
    for (DWORD n=0; n<nLen; n++)
        crc= (crc>>8) ^ panCRC32Table[(crc & 0xFF) ^ cBuf[n]];
    return ~crc;
}
*/
DWORD inline __declspec (naked) __fastcall CalcCRC32(UINT        nLen       ,
                                                     const BYTE* cBuf       ,
                                                     DWORD       nInitVal= 0 ) // used to calc CRC of chained bufs
{
        _asm mov    eax, [esp+4]         // param3: nInitVal
        _asm jecxz  p2                   // __fastcall param1 ecx: nLen
        _asm not    eax
        _asm push   esi
        _asm push   ebp
        _asm mov    esi, edx             // __fastcall param2 edx: cBuf
        _asm xor    edx, edx
        _asm mov    ebp, panCRC32Table
        _asm cld

    p1: _asm mov    dl , al
        _asm shr    eax, 8
        _asm xor    dl , [esi]
        _asm xor    eax, [ebp+edx*4]
        _asm inc    esi
        _asm loop   p1

        _asm pop    ebp
        _asm pop    esi
        _asm not    eax
    p2: _asm ret    4                    // eax- returned value. 4 because there is 1 param in stack
}

// test code:

#include "mmSystem.h"                      // timeGetTime
#pragma comment(lib, "Winmm.lib" )

InitCRC32Table();

BYTE* x= new BYTE[1000000];
for (int i= 0; i<1000000; i++) x[i]= 0;

DWORD d1= ::timeGetTime();

for (i= 0; i<1000; i++)
    CalcCRC32(1000000, x, 0);

DWORD d2= ::timeGetTime();

TRACE("%d\n", d2-d1);
1 голос
/ 14 сентября 2009

Хорошие ответы. Я бы сказал «Да», ЕСЛИ вы уже сделали настройку производительности, как это , и теперь вы находитесь в положении

  1. Зная (не догадываясь), что какая-то конкретная горячая точка занимает более 30% вашего времени,

  2. видя, какой язык ассемблера сгенерировал для него компилятор, после всех попыток заставить его генерировать оптимальный код,

  3. зная, как улучшить этот ассемблерный код.

  4. готов отказаться от некоторой переносимости.

Компиляторы не знают всего, что вы знаете, поэтому они защищены и не могут воспользоваться тем, что вы знаете.

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

1 голос
/ 11 сентября 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...