Что случилось с ним? То, что Вы пересматриваете сборщик "мусора" и средство выделения памяти, у которых между ними есть намного большая идея о фактическом использовании памяти Вашего приложения во времени выполнения, чем Вы.
I ' Я бы сказал, что оно того не стоит. Я работаю над программным обеспечением, которое выполняет 3D-рендеринг в реальном времени (т. Е. Рендеринг без помощи графического процессора). Я действительно широко использую встроенные функции компилятора SSE - много уродливого кода, заполненного __ mm_add_ps ()
и другими, - но мне не нужно было перекодировать функцию в сборке в течение очень долгого времени.
Мой опыт показывает, что хорошие современные оптимизирующие компиляторы чертовски эффективны при сложной оптимизации на микроуровне. Они будут выполнять сложные преобразования цикла, такие как переупорядочивание, разворачивание, конвейерная обработка, блокировка, мозаика, заедание, разделение и тому подобное. Они запланируют инструкции, чтобы конвейер оставался заполненным, векторизовали простые циклы и внедрили несколько интересных хитростей. Современные компиляторы - невероятно очаровательные звери.
Сможете ли вы победить их? Хорошо обязательно, учитывая, что они выбирают оптимизацию для использования с помощью эвристики, они неизбежно иногда ошибаются. Но я обнаружил, что гораздо лучше оптимизировать сам код, глядя на картину в целом. Я размещаю свои структуры данных наиболее удобным для кеширования способом? Я делаю что-то необычное, что вводит компилятор в заблуждение? Могу ли я что-нибудь немного переписать, чтобы компилятор лучше подсказывал? Не лучше ли мне что-то пересчитать, а не хранить? Не удалось ли вставить справку по предварительной выборке? У меня где-то есть ложное совместное использование кеша? Есть ли небольшая оптимизация кода, которую компилятор считает небезопасной, но здесь допустима (например, преобразование деления в умножение на обратное)?
Мне нравится работать с компилятором, а не против него. Пусть он позаботится об оптимизации на микроуровне, чтобы вы могли сосредоточиться на оптимизации на мезоуровне.
Единственный возможный ответ на этот вопрос: да, если есть прирост производительности, который актуален и полезен.
Я предполагаю, что на самом деле вопрос должен заключаться в следующем: можете ли вы получить значительный прирост производительности используя ассемблер в программе C / C ++?
Ответ - да.
Случаи, когда вы получаете значимое повышение производительности, вероятно, уменьшились за последние 10-20 лет, поскольку библиотеки и компиляторы улучшились, но для такой архитектуры, как x86, в частности, ручная оптимизация в определенных приложениях (особенно связанных с графикой) это может быть выполнено.
Но, как и все остальное, не оптимизируйте, пока вам не понадобится.
Я бы сказал, что оптимизация алгоритмов и написание высокоэффективного C (в частности) даст гораздо больший прирост производительности при меньших затратах времени, чем переписывание на языке ассемблера в подавляющем большинстве случаев.
Сложность в том, можете ли вы сделать работу по оптимизации лучше, чем компилятор, учитывая архитектуру современных процессоров. Если вы разрабатываете для простого процессора (например, для встроенных систем), вы можете провести разумную оптимизацию, но для конвейерной архитектуры оптимизация намного сложнее, так как вам нужно понять, как работает конвейерная обработка.
Итак, учитывая, что , если вы можете выполнить эту оптимизацию, и вы работаете над чем-то, что профилировщик сообщает вам слишком медленно, и это часть, которая должна быть максимально быстрой, тогда да, оптимизация имеет смысл.
Вам понадобится профиль, который вы получите с помощью инструмента профилирования, прежде чем вы узнаете. Некоторые программы проводят все свое время в ожидании базы данных или у них просто нет сконцентрированного времени выполнения на небольшой площади. Без этого сборка не очень помогает.
Существует эмпирическое правило, согласно которому 90% времени выполнения приходится на 10% кода. Вам действительно нужно одно очень сильное узкое место, а оно есть не в каждой программе.
Кроме того, машины теперь настолько быстры, что некоторые из низко висящих плодов были съедены, так сказать, компиляторами и ядрами ЦП. Например, вы пишете код лучше, чем компилятор, и вдвое сокращаете количество инструкций. Даже в этом случае, если вы в конечном итоге выполните такое же количество обращений к памяти, и если они будут узким местом, вы можете не выиграть.
Конечно, вы можете начать предварительную загрузку регистров в предыдущих итерациях цикла, но компилятор, вероятно, уже пытается это сделать.
Изучение сборки действительно более важно как способ понять, чем на самом деле является машина, а не как способ обыграть компилятор. Но попробуйте!
Есть одна область, где оптимизация сборки все еще регулярно выполняется - встроенное программное обеспечение . Эти процессоры обычно не очень мощные и имеют множество архитектурных особенностей, которые не могут быть использованы компилятором для оптимизации. Тем не менее, это следует делать только для особо ограниченных областей кода и , это должно быть очень хорошо задокументировано.
Я предполагаю, что вы профилировали свой код и обнаружили небольшой цикл, который занимает большую часть время.
Сначала попробуйте перекомпилировать с более агрессивной оптимизацией компилятора, а затем перепрофилировать. Если вы по желанию запускаете все оптимизации компилятора, но вам все еще нужно больше производительности, я рекомендую взглянуть на сгенерированную сборку.
Что я обычно делаю после просмотра кода сборки для функции, так это смотрю, как я могу изменить код C, чтобы заставить компилятор писать лучшую сборку. Преимущество этого способа,
определенно да!
Вот демонстрация вычисления 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);
Для типичного разработчика небольшого магазина, пишущего приложение, компромисс между приростом производительности и усилиями почти никогда не оправдывает написание ассемблера. Даже в ситуациях, когда сборка может удвоить скорость некоторого узкого места, усилия часто неоправданны. В более крупной компании это может быть оправдано, если вы "парень производительности".
Однако для автора библиотеки даже небольшие улучшения с большими усилиями часто оправданы, потому что это экономит время тысяч разработчиков и пользователей, которые использовать библиотеку в конце. Тем более для разработчиков компиляторов. Если вы можете добиться 10% -ного выигрыша в эффективности функции базовой системной библиотеки, это может буквально сэкономить тысячелетия (или больше) времени автономной работы, распределенной среди вашей пользовательской базы.
Я бы сказал, что для большинства людей и большинства приложений это того не стоит. Компиляторы очень хороши в оптимизации именно для той архитектуры, для которой они компилируются.
Это не означает, что оптимизация в сборке не является необоснованной. Многие математические операции и низкоуровневый интенсивный код часто оптимизируются с помощью специальных инструкций ЦП, таких как SSE * и т. Д., Чтобы избежать использования сгенерированных компилятором инструкций / регистров. В конце концов, человек точно знает суть программы. Компилятор может предполагать только так много.
Я бы сказал, что если вы не на том уровне, где вы знаете, что ваша собственная сборка будет быстрее, тогда я позволю компилятору сделать тяжелую работу.
Не забывайте, что при переписывании в сборке вы теряете переносимость. Сегодня вам все равно, но завтра ваши клиенты могут захотеть, чтобы ваше программное обеспечение было на другой платформе, и им эти фрагменты сборки действительно повредят.
Good answers. I would say "Yes" IF you have already done performance tuning like this, and you are now in the position of
KNOWING (not guessing) that some particular hot-spot is taking more than 30% of your time,
seeing just what assembly language the compiler generated for it, after all attempts to make it generate optimal code,
knowing how to improve on that assembler code.
being willing to give up some portability.
Compilers do not know everything you know, so they are defensive and cannot take advantage of what you know.
As one example, they write subroutine entry and exit code in a general way that works no matter what the subroutine contains. You, on the other hand, may be able to hand-code little routines that dispense with frame pointers, saving registers, and stuff like that. You're risking bugs, but it is possible to beat the compiler.