Оптимизирует некоторые функции с помощью Assembler в программе на C/С++, действительно ли это стоит?

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

Вопросы:

  • Оптимизирует некоторые функции с помощью сборки в C/С++ действительно стоит того?

  • Есть ли достаточный выигрыш в   производительность при оптимизации C/С++   с Ассамблеей с сегодняшними   современные компиляторы?


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

Ответ 1

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

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

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

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

Ответ 2

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

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

Ответ - да.

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

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

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

Ответ 3

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

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

Ответ 4

Может

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

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

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

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

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

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

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

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

Реализация ассемблера - это не 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);

Ответ 9

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

Это не означает, что оптимизация сборки не является необоснованной. Многие математические и низкоуровневые интенсивные коды часто оптимизируются с использованием определенных инструкций ЦП, таких как SSE * и т.д., Для преодоления генерируемой компилятором инструкции/использования регистров. В конце концов, человек точно знает суть программы. Компилятор может принимать только так много.

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

Ответ 10

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

Ответ 11

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

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

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

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

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

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

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