Pull to refresh

Comments 73

банальная цитата о преждевременной оптимизации
UFO just landed and posted this here
Если вы обнуляете данные гигабайтами, то вы что-то делаете не так :)
Мне очень трудно представить когда это является узким местом в программе.
UFO just landed and posted this here
практически все известные мне менеджеры памяти не обнуляют страницы, а помечают их как свободные.
UFO just landed and posted this here
Да, вы правы, на нулевом приоритете крутится поток MmZeroPageThread, который билдид список MmZeroedPageListHead.
Но никак не гигабайтами, и только во время абсолютного простоя, ибо ниже приоритета 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


UFO just landed and posted this here
еще раз.
речь идёт о производительности обнуления памяти.
программа работает месяц и за это время очистила пару гигов памяти, нужно ли оптимизировать, раз очищаются такие здоровые объемы памяти? Или это тоже считается «узким местом»? По-моему ответ очевиден, и не стоит передергивать мои слова :)

А на первый вопрос ответ можно глянуть в открытых сорцах ядра винды:

    //
    // 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);
UFO just landed and posted this here
> Я вот на это ответил
а вторую часть не такого уж длинного комментария не заметили, или это так и надо выдирать фразы из контекста? :)
> Покрутите немного колесиком мыши…
опять же, я не имею в виду конкретное приложение (у вас — виндовый MM), а отвечаю абстрактно на простой вопрос («Если обнуляет медленно и не постоянно, то не считается?»)
UFO just landed and posted this here
Допустим это приложение очень быстро чем нибудь управляет, в этом случае нужно оптимизировать обнуление этого несчастного килобайта, будь он неладен.

Если этот несчастный нулевой килобайт — итоговая информация, то оптимизировать надо протокол, а не код и не алгоритм :) Если же поверх этого нулевого килобайта перед выдачей пишется что-то осмысленное, то время, потраченное на вычисления будет несоизмеримо больше времени обнуления.
UFO just landed and posted this here
Множество ситуаций, где обнуление памяти — вполне осмысленная операция и множество ситуаций, где скорость этого обнуления критична по времени, не пересекаются.
UFO just landed and posted this here
Ну тогда приведите пример, где обнуление памяти является вполне осмысленной операцией и одновременно с этим заметно влияет на общую скорость.
UFO just landed and posted this here
И это обнуление внесёт заметный вклад в общее время операции?
UFO just landed and posted this here
Вряд ли быстрее, чем этот ключ будет передаваться от клиента, даже если это RPC внутри машины, а не сеть.

Да и случай с обнулением ключа мне кажется притянутым — зачем его обнулять? приложение защищается от самого себя?
UFO just landed and posted this here
Обнулять массивы можно. Речь о том, что это обнуление либо изначально не является критичным по времени, либо его можно сделать таковым, не прибегая к оптимизации самой реализации обнуления.
UFO just landed and posted this here
Плохо — не должно. Хорошо — тоже :) Потому что с практической точки зрения оно фиолетово даже если бы компилятор не сводил всё к memset'у. И дело не в быстрых процессорах, а в соотношении количества обнулений с количеством и стоимостью вычислений поверх обнулённого. Оптимизировать обнуление имеет смысл только с эстетической точки зрения.
Привет из 2015 ,) 4 года назад я как-то пропустил эту полемику и теперь жуть, как интересно, что было в удаленных НЛО комментариях :) Только криптография? То же касается ветки ниже про виртуальную память.
Есть мнение что всё что можно оптимизировать можно оптимизировать.
oops: простите, нужно оптимизировать! :)
UFO just landed and posted this here
Это даже не рекурсия, а просто тождественно истинное утверждение :)
агу, компилятор НЛО его может оптимизировать, и выкинет :)
Да пусть сколько влезет обнуляет, хоть петабайты. Его вообще можно убрать и ничего не поломается ;) разве что VirtualAlloc'и чуть дольше будут отрабатывать, что опять же является критичным местом только в очень неадекватном коде.
UFO just landed and posted this here
Вообще-то VirtualAlloc работает с виртуальными страницами (если только не указан MEM_PHYSICAL, что есть экзотика). А хотел я сказать то, что при MEM_COMMIT он by design возвращает обнулённый кусок памяти. Нафига — по идее ради того чтобы не нарваться на ошмётки данных/паролей из чужого отработавшего процесса. Обсуждаемый idle thread нужен только для того чтобы VirtaulAlloc в большинстве случаев получал уже обнулённый кусок, когда выделяемая страница попадает на физическую память.
UFO just landed and posted this here
Физические страницы, принадлежавшие ранее одному приложению, не могут быть прочитаны другим приложением, пока они не попадут в его адресное пространство. А попасть они туда могут только через VirtualAlloc.

Таким образом, если обсуждаемого idle thread'а не будет или он не получит процессорного времени между смертью первого приложения и запуском второго (точнее между VirtualFree первого и VirtualAlloc второго), обнулением придётся заниматься VirtualAlloc'у.

Обнуляющий поток нужен только для того, чтобы в большинстве случаев VirtualAlloc получал уже обнулённую физическую страницу и не тратил на это обнуление процессорное время выделяющего память приложения.
Тут дело в том, что из-за нулевого приоритета этого обнуляющего потока возможны ситуации, когда VirtualAlloc нарвётся на необнулённую страницу — и её всё равно придётся обнулять, но уже не из этого потока, и из VirtualAlloc'а (его kernel-mode части) — т.е. эта логика в VirtualAlloc'е по-любому есть. Назначение idle thread'а — избавить большинство VirtualAlloc'ов от этого обнуляющего цикла, выполняя обнуление в фоне и помечая физические страницы как обнулённые. Если этот флаг стоит — VirtualAlloc ничего не делает. Если флага нет, обнулением занимается VirtualAlloc.
UFO just landed and posted this here
UFO just landed and posted this here
Ок, не только в VirtualAlloc, но и во всех остальных ситуациях, выделяющих физическую страницу под кусок адресного пространества, будет стоять проверка на обнулённость страницы и если флага обнулённости не окажется, будет выполняться обнуление на месте.

Суть моего повествования в том что idle thread может не успеть ничего обнулить — и, таким образом, может быть вообще исключен из системы, не влияя на её работоспособность. При этом приложения будут и дальше получать обнулённые физические страницы в результате VirtualAlloc'ов, page fault'ов и т.п. Нзначение этого idle thread'а — сугубо оптимизирующее, а не краеугольнее.
Вася и Петя зателнетелись на один сервер. Вася отредактировал секретный файлик и отлогинился. Хитрый Петя, написал программу, затребовал много памяти, и нашел в памяти файлик отредактированный Васей… Ведь менеджеры памяти не обнуляют свободные страницы, как Вы думаете?
мне кажется если бы вся освобожденная память обнулялась, то в выделенном новом куске должны быть одни нули, не так ли? В жизни такого не наблюдал, более того пример выше с файликом не так далек от реальности.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Ну например, многодорожечный аудиомикшер, в программе есть несколько десятков звукогенераторов, которые в реальном времени обрабатывают, каждый, скажем, под 192 КГц / 2 канала = 384000 сэмплов * 4 байта (или даже 8, щас модно на double микширование делать) в секунду, и перед каждым заполнением им нужно занулять свои буферы, потом суммировать туда что-то (алгоритм не всегда позволяет просто перезаписывать буфер). Естественно, это всё равно будет не самое узкое место в программе, но пара % загрузки процессора улетит тупо на нагрев воздуха. И если «преждевременно» не думать о таких мелочах, то потом проект уходит на профилирование и оптимизацию на месяц.
никогда этим не интересовался, но неужели эти 400к сэмплов нужны одновременно?
простой подсчет показывает что только под эти буферы нужно будет 200-300 метров памяти, неужели микшер столько кушает?
Конечно нет, если микшер не на иммутабельных списках.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Интересно. Спасибо.
А попробуйте, пожалуйста, для расширения мистического опыта добавить в ключи компиляции "-march=native -mtune=native" — интересно, появятся ли дополнительные оптимизации вроде использования SSE. Только не забудьте написать, какая у вас машина.
Может я чего не понимаю, но не слишком ли брутально не освобождать гиг памяти?
Так программа завершится, и вся выделенная память будет уничтожена (т.к. будет уничтожена куча приложения). К слову — уничтожение кучи происходит быстрее, чем отдельных её составляющих, а потом и самой кучи ;)
точно, моя невнимательность:)
Только вот memory leak детекторы будут ругаться все равно. В данном конкреном случае это не так важно, потому что это тест, а вот в реальном приложении из-за таких ошибок можно не заметить реальный лик. К тому же разве имеет особое значение для вашего приложения сколько времени займет его завершение?
Это ясно, я и не призывал не удалять память :) А вот насчёт времени завершения — не соглашусь. Тратится процессорное время на то, что в общем-то не нужно. С точки зрения логики — зачем вызывать удаление памяти, если завершается приложение? А потом получаются Оперы и Фаерфоксы, котрые после закрытия окна ещё очищают память в течение значительного времени, тратя ресурсы. Но проблема далеко не тривиальная, поэтому её игнорируют.
«К тому же разве имеет особое значение для вашего приложения сколько времени займет его завершение?»

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

ps: я не в коей мере не занудствую просто при написании больших программных комплексов (а по хорошему в любой программе) надо учитывать такие моменты.
всё так, но только в куче максимальный размер куска — 512Kb, в x64 — 1 метр, для больших кусков (если размер кучи не лимитирован) используется прямое выделение памяти через VirtualAlloc, и ваше утверждение про скорость уничтожения становится неверным :)
Вы думаете, что вызвать delete/free для каждого объекта, а затем удаление кучи будет не медленнее удаления сразу всей кучи? Да, если объекты большие и их немного, то разница незаметна. А вот если выделено много маленьких объектов, то удаление всей кучи происходит значительно быстрее.
ну речь идёт об освобождение гига памяти)
Ну, я там поставил «к слову» именно с целью уточнить, что не в данном случае, но возможны варианты. Наверное недосточно точно выразился.
В MSVS 2010 все наоборот:

// 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 или таки используется одна из специализаций:
; 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; ++)
    *= '\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 оптимизатор более продвинутый.
Не, это давно появилось (в 2к5, кажется) — ещё тогда некоторые ругались, пытаясь отвязать CRT-less прогу от memset'a.
Во первых у вас погрешность измерения сопоставима с результатами измерения. Используйте rtdsc.
Во вторых мемсетить лучше double* через SSE и мимо кэша (movntpd).
В третьих поиграйтесь лучше с memcpy() — это более полезно…
Да мне тоже не понятно, как человек знающий зачем есть «disas» в gdb может измерять время через ./time prog
Есть valgrind, есть vtune, которые (помимо всего прочего) отлично меряют затраченные циклы процессора.
Поговорку про преждевременную оптимизацию не слышали?
Первый комментарий в топике не читали?
Конечно нет, если микшер не на иммутабельных списках.
Каждый моно канал кушает размер asio буфера, ну или другой подсистемы; от 256 (асио и крутая звуковуха) до пары десятков тысяч (вин мм) отсчетов, даблов в данном случае. Чем меньше, чем лучше — меньше задержка, и тем меньше нужно памяти. Плюс буфер для суммы, который конечно не нужно обнулять — он перезаписывается блин.
Sign up to leave a comment.

Articles