Информация об изменениях

Сообщение Re: Почему так? от 12.09.2021 23:11

Изменено 13.09.2021 9:14 watchmaker

Re: Почему так?
Здравствуйте, TailWind, Вы писали:

TW>Для каждого чтения выделяю буфер размера Buf_Size с помощью vector<UCHAR> vbuf(Buf_Size);

TW>И удивился что, при размере буфера 0x80000 стало дольше работать
TW>Почему?

Для больших размеров буфера может меняется поведение в двух местах:
* OC может по разному читать файл (изменить политику read-ahead, например);
* Аллокатор может перестать управлять памятью самостоятельно, а просто передавать запросы ОС (вызывая mmap напрямую с требуемым размером).

Первое не про память, а вот второе напрямую влияет на скорость выделения и освобождения памяти. Если раньше аллокатор просто менял бит занятости у какого-то элемента в списке блоков (да поддерживал какую-то простую структуру для поиска этих блоков), то теперь это честный поход в ядро ОС.
И все популярные аллокаторы (кстати, с каким ты тестируешь?) так делают. Ибо выигрыша от собственного управления большими блоками не выходит: короткоживущие большие блоки — не типичный сценарий — в хорошей программе их либо мало, либо живут они долго. Но это же значит, что выделение и освобождение большого блока в цикле для аллокаторов будет плохим сценарием.


TW>Если заменяю vector на:

TW>void *buf = malloc(Buf_Size); free(buf);
TW>работает моментально
TW>Почему?

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

Что компиляторы постоянно и делают: https://godbolt.org/z/9jse3Era3
Как видно, пару free(malloc(...)) компилятор тут же удаляет целиком. Отсюда и быстрая работа.
Неиспользуемый std::vector<char>(n, '\0') или занулённый результат malloc компилятор также может удалить (оставив бросание исключения, в случае отрицательного размера, например).
Но это получается у него не всегда: обычно у него не получает разглядеть через шаблоны замысел кода (что это всего-лишь временный буфер, к которому нет обращений из других мест или потоков), либо для этого нужно либо выкручивать уровень оптимизации. (но иногда получается: https://godbolt.org/z/7cxe6zYob)




TW>vector каждый UCHAR прописывает нулём в цикле?


Да.
Поэтому в популярных реализациях STL (и в стороннем коде, реализующем всякие string-like или vector-like структуры данных) есть различные расширения для сценария, когда нужно выделить память в контейнере, в которую дальше будет идти запись, и которую поэтому можно предварительно не занулять (примеры libc++,catboost,abseil,protobuf).

Вероятно, в каком-то виде их даже узаконят в стандарте языка http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1072r5.html (но, как видно, многие не ждут и пользуются ими уже сейчас).
Re: Почему так?
Здравствуйте, TailWind, Вы писали:

TW>Для каждого чтения выделяю буфер размера Buf_Size с помощью vector<UCHAR> vbuf(Buf_Size);

TW>И удивился что, при размере буфера 0x80000 стало дольше работать
TW>Почему?

Для больших размеров буфера может меняется поведение в двух местах:
* OC может по разному читать файл (изменить политику read-ahead, например);
* Аллокатор может перестать управлять памятью самостоятельно, а просто передавать запросы ОС (вызывая mmap напрямую с требуемым размером).

Первое не про память, а вот второе напрямую влияет на скорость выделения и освобождения памяти. Если раньше аллокатор просто менял бит занятости у какого-то элемента в списке блоков да поддерживал какую-то простую (или не очень) структуру для поиска этих блоков, то теперь это честный поход в ядро ОС.
И так делают все популярные аллокаторы (кстати, с каким ты тестируешь?). Ибо выигрыша от собственного управления большими блоками не выходит: короткоживущие большие блоки не считаются типичным сценарием — в хорошей программе их либо мало, либо живут они долго. Но это же значит, что выделение и освобождение большого блока в цикле для аллокаторов всегда будет идти по дорогой ветке.


TW>Если заменяю vector на:

TW>void *buf = malloc(Buf_Size); free(buf);
TW>работает моментально
TW>Почему?

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

Что компиляторы постоянно и делают: https://godbolt.org/z/9jse3Era3
Как видно, пару free(malloc(...)) компилятор тут же удаляет целиком. Отсюда и быстрая работа.
Неиспользуемый std::vector<char>(n, '\0') или занулённый результат malloc компилятор также может удалить (оставив бросание исключения, в случае недопустимого размера, например).
Но это получается у него не всегда: обычно у него не получается разглядеть через шаблоны замысел кода (что это всего-лишь временный буфер, к которому нет обращений из других мест или потоков), либо для этого нужно либо выкручивать уровень оптимизации. (но иногда получается: https://godbolt.org/z/7cxe6zYob)




TW>vector каждый UCHAR прописывает нулём в цикле?


Да.
Поэтому в популярных реализациях STL (и в стороннем коде, реализующем всякие string-like или vector-like структуры данных) есть различные расширения для сценария, когда нужно выделить память в контейнере, в которую дальше будет идти запись, и которую поэтому можно предварительно не занулять (примеры libc++,catboost,abseil,protobuf).

Вероятно, в каком-то виде их даже узаконят в стандарте языка: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1072r5.html Но, как видно, многие не ждут и пользуются этими расширениями уже сейчас.