Comments 73
банальная цитата о преждевременной оптимизации
Если вы обнуляете данные гигабайтами, то вы что-то делаете не так :)
Мне очень трудно представить когда это является узким местом в программе.
Мне очень трудно представить когда это является узким местом в программе.
практически все известные мне менеджеры памяти не обнуляют страницы, а помечают их как свободные.
Да, вы правы, на нулевом приоритете крутится поток MmZeroPageThread, который билдид список MmZeroedPageListHead.
Но никак не гигабайтами, и только во время абсолютного простоя, ибо ниже приоритета 0 нету ничего :)
Так что это слабо опровергает мои слова о том, что необходимо заострять внимание на этом аспекте.
Например, в вышеупомянутой винде код очистки крайне прост.
Дефолтный код:
И второй вариант с XMMI (extended memory managment):
Но никак не гигабайтами, и только во время абсолютного простоя, ибо ниже приоритета 0 нету ничего :)
Так что это слабо опровергает мои слова о том, что необходимо заострять внимание на этом аспекте.
Например, в вышеупомянутой винде код очистки крайне прост.
Дефолтный код:
.text:00430364 __fastcall KiZeroPages(x, x) proc near ; CODE XREF: MiZeroWorkerPages(x,x)+10Dp
.text:00430364 ; KiXMMIZeroPages(x,x)+B5j ...
.text:00430364 push edi
.text:00430365 xor eax, eax
.text:00430367 mov edi, ecx
.text:00430369 mov ecx, edx
.text:0043036B shr ecx, 2
.text:0043036E rep stosd
.text:00430370 pop edi
.text:00430371 retn
.text:00430371 __fastcall KiZeroPages(x, x) endp
И второй вариант с XMMI (extended memory managment):
.text:00430288 __fastcall KiXMMIZeroPagesNoSave(x, x) proc near
.text:00430288 ; CODE XREF: KiXMMIZeroPages(x,x)+84p
.text:00430288 ; DATA XREF: KiInitMachineDependent()+1F0o
.text:00430288
.text:00430288 var_4 = dword ptr -4
.text:00430288
.text:00430288 xorps xmm0, xmm0
.text:0043028B shr edx, 6
.text:0043028E
.text:0043028E loc_43028E: ; CODE XREF: KiXMMIZeroPagesNoSave(x,x)+19j
.text:0043028E movntps xmmword ptr [ecx], xmm0
.text:00430291 movntps xmmword ptr [ecx+10h], xmm0
.text:00430295 movntps xmmword ptr [ecx+20h], xmm0
.text:00430299 movntps xmmword ptr [ecx+30h], xmm0
.text:0043029D add ecx, 40h
.text:004302A0 dec edx
.text:004302A1 jnz short loc_43028E
.text:004302A3 sfence
.text:004302A6 xchg edx, [esp+var_4]
.text:004302AA retn
.text:004302AA __fastcall KiXMMIZeroPagesNoSave(x, x) endp
еще раз.
речь идёт о производительности обнуления памяти.
программа работает месяц и за это время очистила пару гигов памяти, нужно ли оптимизировать, раз очищаются такие здоровые объемы памяти? Или это тоже считается «узким местом»? По-моему ответ очевиден, и не стоит передергивать мои слова :)
А на первый вопрос ответ можно глянуть в открытых сорцах ядра винды:
речь идёт о производительности обнуления памяти.
программа работает месяц и за это время очистила пару гигов памяти, нужно ли оптимизировать, раз очищаются такие здоровые объемы памяти? Или это тоже считается «узким местом»? По-моему ответ очевиден, и не стоит передергивать мои слова :)
А на первый вопрос ответ можно глянуть в открытых сорцах ядра винды:
//
// The following code sets the current thread's base priority to zero
// and then sets its current priority to zero. This ensures that the
// thread always runs at a priority of zero.
//
KeSetPriorityZeroPageThread (0);
> Я вот на это ответил
а вторую часть не такого уж длинного комментария не заметили, или это так и надо выдирать фразы из контекста? :)
> Покрутите немного колесиком мыши…
опять же, я не имею в виду конкретное приложение (у вас — виндовый MM), а отвечаю абстрактно на простой вопрос («Если обнуляет медленно и не постоянно, то не считается?»)
а вторую часть не такого уж длинного комментария не заметили, или это так и надо выдирать фразы из контекста? :)
> Покрутите немного колесиком мыши…
опять же, я не имею в виду конкретное приложение (у вас — виндовый MM), а отвечаю абстрактно на простой вопрос («Если обнуляет медленно и не постоянно, то не считается?»)
Допустим это приложение очень быстро чем нибудь управляет, в этом случае нужно оптимизировать обнуление этого несчастного килобайта, будь он неладен.
Если этот несчастный нулевой килобайт — итоговая информация, то оптимизировать надо протокол, а не код и не алгоритм :) Если же поверх этого нулевого килобайта перед выдачей пишется что-то осмысленное, то время, потраченное на вычисления будет несоизмеримо больше времени обнуления.
Если этот несчастный нулевой килобайт — итоговая информация, то оптимизировать надо протокол, а не код и не алгоритм :) Если же поверх этого нулевого килобайта перед выдачей пишется что-то осмысленное, то время, потраченное на вычисления будет несоизмеримо больше времени обнуления.
Множество ситуаций, где обнуление памяти — вполне осмысленная операция и множество ситуаций, где скорость этого обнуления критична по времени, не пересекаются.
Ну тогда приведите пример, где обнуление памяти является вполне осмысленной операцией и одновременно с этим заметно влияет на общую скорость.
И это обнуление внесёт заметный вклад в общее время операции?
Обнулять массивы можно. Речь о том, что это обнуление либо изначально не является критичным по времени, либо его можно сделать таковым, не прибегая к оптимизации самой реализации обнуления.
Плохо — не должно. Хорошо — тоже :) Потому что с практической точки зрения оно фиолетово даже если бы компилятор не сводил всё к memset'у. И дело не в быстрых процессорах, а в соотношении количества обнулений с количеством и стоимостью вычислений поверх обнулённого. Оптимизировать обнуление имеет смысл только с эстетической точки зрения.
Есть мнение что всё что можно оптимизировать можно оптимизировать.
Да пусть сколько влезет обнуляет, хоть петабайты. Его вообще можно убрать и ничего не поломается ;) разве что VirtualAlloc'и чуть дольше будут отрабатывать, что опять же является критичным местом только в очень неадекватном коде.
Вообще-то VirtualAlloc работает с виртуальными страницами (если только не указан MEM_PHYSICAL, что есть экзотика). А хотел я сказать то, что при MEM_COMMIT он by design возвращает обнулённый кусок памяти. Нафига — по идее ради того чтобы не нарваться на ошмётки данных/паролей из чужого отработавшего процесса. Обсуждаемый idle thread нужен только для того чтобы VirtaulAlloc в большинстве случаев получал уже обнулённый кусок, когда выделяемая страница попадает на физическую память.
Физические страницы, принадлежавшие ранее одному приложению, не могут быть прочитаны другим приложением, пока они не попадут в его адресное пространство. А попасть они туда могут только через VirtualAlloc.
Таким образом, если обсуждаемого idle thread'а не будет или он не получит процессорного времени между смертью первого приложения и запуском второго (точнее между VirtualFree первого и VirtualAlloc второго), обнулением придётся заниматься VirtualAlloc'у.
Обнуляющий поток нужен только для того, чтобы в большинстве случаев VirtualAlloc получал уже обнулённую физическую страницу и не тратил на это обнуление процессорное время выделяющего память приложения.
Таким образом, если обсуждаемого idle thread'а не будет или он не получит процессорного времени между смертью первого приложения и запуском второго (точнее между VirtualFree первого и VirtualAlloc второго), обнулением придётся заниматься VirtualAlloc'у.
Обнуляющий поток нужен только для того, чтобы в большинстве случаев VirtualAlloc получал уже обнулённую физическую страницу и не тратил на это обнуление процессорное время выделяющего память приложения.
Тут дело в том, что из-за нулевого приоритета этого обнуляющего потока возможны ситуации, когда VirtualAlloc нарвётся на необнулённую страницу — и её всё равно придётся обнулять, но уже не из этого потока, и из VirtualAlloc'а (его kernel-mode части) — т.е. эта логика в VirtualAlloc'е по-любому есть. Назначение idle thread'а — избавить большинство VirtualAlloc'ов от этого обнуляющего цикла, выполняя обнуление в фоне и помечая физические страницы как обнулённые. Если этот флаг стоит — VirtualAlloc ничего не делает. Если флага нет, обнулением занимается VirtualAlloc.
Ок, не только в VirtualAlloc, но и во всех остальных ситуациях, выделяющих физическую страницу под кусок адресного пространества, будет стоять проверка на обнулённость страницы и если флага обнулённости не окажется, будет выполняться обнуление на месте.
Суть моего повествования в том что idle thread может не успеть ничего обнулить — и, таким образом, может быть вообще исключен из системы, не влияя на её работоспособность. При этом приложения будут и дальше получать обнулённые физические страницы в результате VirtualAlloc'ов, page fault'ов и т.п. Нзначение этого idle thread'а — сугубо оптимизирующее, а не краеугольнее.
Суть моего повествования в том что idle thread может не успеть ничего обнулить — и, таким образом, может быть вообще исключен из системы, не влияя на её работоспособность. При этом приложения будут и дальше получать обнулённые физические страницы в результате VirtualAlloc'ов, page fault'ов и т.п. Нзначение этого idle thread'а — сугубо оптимизирующее, а не краеугольнее.
Вася и Петя зателнетелись на один сервер. Вася отредактировал секретный файлик и отлогинился. Хитрый Петя, написал программу, затребовал много памяти, и нашел в памяти файлик отредактированный Васей… Ведь менеджеры памяти не обнуляют свободные страницы, как Вы думаете?
Ну например, многодорожечный аудиомикшер, в программе есть несколько десятков звукогенераторов, которые в реальном времени обрабатывают, каждый, скажем, под 192 КГц / 2 канала = 384000 сэмплов * 4 байта (или даже 8, щас модно на double микширование делать) в секунду, и перед каждым заполнением им нужно занулять свои буферы, потом суммировать туда что-то (алгоритм не всегда позволяет просто перезаписывать буфер). Естественно, это всё равно будет не самое узкое место в программе, но пара % загрузки процессора улетит тупо на нагрев воздуха. И если «преждевременно» не думать о таких мелочах, то потом проект уходит на профилирование и оптимизацию на месяц.
никогда этим не интересовался, но неужели эти 400к сэмплов нужны одновременно?
простой подсчет показывает что только под эти буферы нужно будет 200-300 метров памяти, неужели микшер столько кушает?
простой подсчет показывает что только под эти буферы нужно будет 200-300 метров памяти, неужели микшер столько кушает?
Конечно нет, если микшер не на иммутабельных списках.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Интересно. Спасибо.
А попробуйте, пожалуйста, для расширения мистического опыта добавить в ключи компиляции "-march=native -mtune=native" — интересно, появятся ли дополнительные оптимизации вроде использования SSE. Только не забудьте написать, какая у вас машина.
А попробуйте, пожалуйста, для расширения мистического опыта добавить в ключи компиляции "-march=native -mtune=native" — интересно, появятся ли дополнительные оптимизации вроде использования SSE. Только не забудьте написать, какая у вас машина.
Может я чего не понимаю, но не слишком ли брутально не освобождать гиг памяти?
Так программа завершится, и вся выделенная память будет уничтожена (т.к. будет уничтожена куча приложения). К слову — уничтожение кучи происходит быстрее, чем отдельных её составляющих, а потом и самой кучи ;)
точно, моя невнимательность:)
Только вот memory leak детекторы будут ругаться все равно. В данном конкреном случае это не так важно, потому что это тест, а вот в реальном приложении из-за таких ошибок можно не заметить реальный лик. К тому же разве имеет особое значение для вашего приложения сколько времени займет его завершение?
Это ясно, я и не призывал не удалять память :) А вот насчёт времени завершения — не соглашусь. Тратится процессорное время на то, что в общем-то не нужно. С точки зрения логики — зачем вызывать удаление памяти, если завершается приложение? А потом получаются Оперы и Фаерфоксы, котрые после закрытия окна ещё очищают память в течение значительного времени, тратя ресурсы. Но проблема далеко не тривиальная, поэтому её игнорируют.
«К тому же разве имеет особое значение для вашего приложения сколько времени займет его завершение?»
Ещё как имеет значение: система может ждать завершения приложения при загрузке, приложение может быть пере запущено для обновления, и т.д., и т.п.
ps: я не в коей мере не занудствую просто при написании больших программных комплексов (а по хорошему в любой программе) надо учитывать такие моменты.
Ещё как имеет значение: система может ждать завершения приложения при загрузке, приложение может быть пере запущено для обновления, и т.д., и т.п.
ps: я не в коей мере не занудствую просто при написании больших программных комплексов (а по хорошему в любой программе) надо учитывать такие моменты.
всё так, но только в куче максимальный размер куска — 512Kb, в x64 — 1 метр, для больших кусков (если размер кучи не лимитирован) используется прямое выделение памяти через VirtualAlloc, и ваше утверждение про скорость уничтожения становится неверным :)
Вы думаете, что вызвать delete/free для каждого объекта, а затем удаление кучи будет не медленнее удаления сразу всей кучи? Да, если объекты большие и их немного, то разница незаметна. А вот если выделено много маленьких объектов, то удаление всей кучи происходит значительно быстрее.
В MSVS 2010 все наоборот:
т.е. через memset выполняется 3-я ветка, а четвертая через цикл.
// TEMPLATE FUNCTION fill
template< class _FwdIt, class _Ty >
inline void _Fill(_FwdIt _First, _FwdIt _Last, const _Ty& _Val)
{ // copy _Val through [_First, _Last)
for (; _First != _Last; ++_First)
*_First = _Val;
}
inline void _Fill(char *_First, char *_Last, int _Val)
{ // copy char _Val through [_First, _Last)
_CSTD memset(_First, _Val, _Last - _First);
}
inline void _Fill(signed char *_First, signed char *_Last, int _Val)
{ // copy signed char _Val through [_First, _Last)
_CSTD memset(_First, _Val, _Last - _First);
}
inline void _Fill(unsigned char *_First, unsigned char *_Last, int _Val)
{ // copy unsigned char _Val through [_First, _Last)
_CSTD memset(_First, _Val, _Last - _First);
}
т.е. через memset выполняется 3-я ветка, а четвертая через цикл.
Это еще не все. Померил время выполнения в разных режимах компиляции (SSE on/off и т.п.) и очень удивился, когда время для всех режимов оказалось одинаковым. Изучение сгенерированного кода показало, что во всех случаях вызывается memset. Даже для того случая, когда в debug версии явно виден цикл (первый вариант функции в моем комментарии выше).
Или цикл как то странно приводится к memset или таки используется одна из специализаций:
Как видно, первая и третья ветка вообще используют один и тот же код, а четвертая — после странной проверки делает все тот же memset.
; 8 : if (mode == 1)
00031 83 ff 01 cmp edi, 1
00034 75 16 jne SHORT $LN5@main
$LN32@main:
; 9 : std::memset(buf, 0, n * sizeof(*buf));
00036 68 00 00 00 40 push 1073741824 ; 40000000H
; 15 : std::fill(buf, buf + n, '\0');
0003b 6a 00 push 0
0003d 56 push esi
0003e e8 00 00 00 00 call _memset
; 16 : return buf[0];
00043 0f be 06 movsx eax, BYTE PTR [esi]
00046 83 c4 0c add esp, 12 ; 0000000cH
00049 5f pop edi
0004a 5e pop esi
; 17 : }
0004b c3 ret 0
$LN5@main:
; 10 : // else if (mode == 2)
; 11 : // bzero(buf, n * sizeof(*buf));
; 12 : else if (mode == 3)
0004c 83 ff 03 cmp edi, 3
; 13 : std::fill(buf, buf + n, 0);
0004f 74 e5 je SHORT $LN32@main
; 14 : else if (mode == 4)
00051 83 ff 04 cmp edi, 4
00054 75 18 jne SHORT $LN26@main
; 15 : std::fill(buf, buf + n, '\0');
00056 8d 86 00 00 00
40 lea eax, DWORD PTR [esi+1073741824]
0005c 3b f0 cmp esi, eax
0005e 74 0e je SHORT $LN26@main
00060 2b c6 sub eax, esi
00062 50 push eax
00063 6a 00 push 0
00065 56 push esi
00066 e8 00 00 00 00 call _memset
0006b 83 c4 0c add esp, 12 ; 0000000cH
$LN26@main:
; 16 : return buf[0];
0006e 0f be 06 movsx eax, BYTE PTR [esi]
00071 5f pop edi
00072 5e pop esi
Как видно, первая и третья ветка вообще используют один и тот же код, а четвертая — после странной проверки делает все тот же memset.
Странная проверка делается чтобы не вызывать memset, если n == 0 (хотя, в данном случае ее можно вообще выкинуть, почему компилятор до этого не додумался непонятно). А цикл да, мистическим образом превращается в memset, более того, следующий код тоже превратится в memset (!):
for( auto p = buf; p != buf + n; ++p )
*p = '\0';
00DA107D lea eax,[esi+40000000h]
00DA1083 cmp esi,eax
00DA1085 je f+95h (0DA1095h)
00DA1087 sub eax,esi
00DA1089 push eax
00DA108A push 0
00DA108C push esi
00DA108D call memset (0DA1D64h)
00DA1092 add esp,0Ch
Выходит в VS2010 оптимизатор более продвинутый.
Во первых у вас погрешность измерения сопоставима с результатами измерения. Используйте rtdsc.
Во вторых мемсетить лучше double* через SSE и мимо кэша (movntpd).
В третьих поиграйтесь лучше с memcpy() — это более полезно…
Во вторых мемсетить лучше double* через SSE и мимо кэша (movntpd).
В третьих поиграйтесь лучше с memcpy() — это более полезно…
Поговорку про преждевременную оптимизацию не слышали?
Конечно нет, если микшер не на иммутабельных списках.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Sign up to leave a comment.
Кто быстрее: memset, bzero или std::fill