standard reparar maximum management installing formatear error after windows memory-management windows-7 windows-10 windows-server-2016

reparar - Rendimiento bajo de Windows 10 en comparación con Windows 7(el manejo de fallas de página no es escalable, severa contención de bloqueo cuando no hay subprocesos> 16)



reparar error memory management windows 10 (2)

Configuramos dos estaciones de trabajo HP Z840 idénticas con las siguientes especificaciones

  • 2 x Xeon E5-2690 v4 @ 2.60GHz (Turbo Boost ON, HT OFF, total de 28 CPU lógicas)
  • Memoria 32GB DDR4 2400, Quad-channel

e instaló Windows 7 SP1 (x64) y Windows 10 Creators Update (x64) en cada uno.

Luego ejecutamos un pequeño punto de referencia de memoria (código a continuación, construido con VS2015 Update 3, arquitectura de 64 bits) que realiza la asignación de memoria sin relleno simultáneamente desde múltiples hilos.

#include <Windows.h> #include <vector> #include <ppl.h> unsigned __int64 ZQueryPerformanceCounter() { unsigned __int64 c; ::QueryPerformanceCounter((LARGE_INTEGER *)&c); return c; } unsigned __int64 ZQueryPerformanceFrequency() { unsigned __int64 c; ::QueryPerformanceFrequency((LARGE_INTEGER *)&c); return c; } class CZPerfCounter { public: CZPerfCounter() : m_st(ZQueryPerformanceCounter()) {}; void reset() { m_st = ZQueryPerformanceCounter(); }; unsigned __int64 elapsedCount() { return ZQueryPerformanceCounter() - m_st; }; unsigned long elapsedMS() { return (unsigned long)(elapsedCount() * 1000 / m_freq); }; unsigned long elapsedMicroSec() { return (unsigned long)(elapsedCount() * 1000 * 1000 / m_freq); }; static unsigned __int64 frequency() { return m_freq; }; private: unsigned __int64 m_st; static unsigned __int64 m_freq; }; unsigned __int64 CZPerfCounter::m_freq = ZQueryPerformanceFrequency(); int main(int argc, char ** argv) { SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); int ncpu = sysinfo.dwNumberOfProcessors; if (argc == 2) { ncpu = atoi(argv[1]); } { printf("No of threads %d/n", ncpu); try { concurrency::Scheduler::ResetDefaultSchedulerPolicy(); int min_threads = 1; int max_threads = ncpu; concurrency::SchedulerPolicy policy (2 // two entries of policy settings , concurrency::MinConcurrency, min_threads , concurrency::MaxConcurrency, max_threads ); concurrency::Scheduler::SetDefaultSchedulerPolicy(policy); } catch (concurrency::default_scheduler_exists &) { printf("Cannot set concurrency runtime scheduler policy (Default scheduler already exists)./n"); } static int cnt = 100; static int num_fills = 1; CZPerfCounter pcTotal; // malloc/free printf("malloc/free/n"); { CZPerfCounter pc; for (int i = 1 * 1024 * 1024; i <= 8 * 1024 * 1024; i *= 2) { concurrency::parallel_for(0, 50, [i](size_t x) { std::vector<void *> ptrs; ptrs.reserve(cnt); for (int n = 0; n < cnt; n++) { auto p = malloc(i); ptrs.emplace_back(p); } for (int x = 0; x < num_fills; x++) { for (auto p : ptrs) { memset(p, num_fills, i); } } for (auto p : ptrs) { free(p); } }); printf("size %4d MB, elapsed %8.2f s, /n", i / (1024 * 1024), pc.elapsedMS() / 1000.0); pc.reset(); } } printf("/n"); printf("Total %6.2f s/n", pcTotal.elapsedMS() / 1000.0); } return 0; }

Sorprendentemente, el resultado es muy malo en Windows 10 CU en comparación con Windows 7. Tracé el resultado a continuación para 1MB de tamaño de fragmento y 8 MB de tamaño de fragmento, variando el número de subprocesos de 2,4, .., hasta 28. Mientras que Windows 7 dio un rendimiento ligeramente peor cuando aumentamos el número de subprocesos, Windows 10 dio una escalabilidad mucho peor.

Hemos intentado asegurarnos de que todas las actualizaciones de Windows se apliquen, actualicen los controladores y modifiquen la configuración del BIOS sin éxito. También ejecutamos el mismo punto de referencia en varias otras plataformas de hardware, y todas dieron una curva similar para Windows 10. Por lo tanto, parece ser un problema de Windows 10.

¿Alguien tiene una experiencia similar, o tal vez know-how sobre esto (tal vez nos perdimos algo?). Este comportamiento ha hecho que nuestra aplicación multiproceso obtenga un rendimiento significativo.

*** EDITADO

Usando https://github.com/google/UIforETW (gracias a Bruce Dawson) para analizar el punto de referencia, descubrimos que la mayor parte del tiempo se gasta en los kernels KiPageFault. Al profundizar en el árbol de llamadas, todo conduce a ExpWaitForSpinLockExclusiveAndAcquire. Parece que la contención de bloqueo está causando este problema.

*** EDITADO

Datos recopilados de Server 2012 R2 en el mismo hardware. Server 2012 R2 también es peor que Win7, pero aún mucho mejor que Win10 CU.

*** EDITADO

Sucede en Server 2016 también. Agregué la etiqueta windows-server-2016.

*** EDITADO

Utilizando información de @ Ext3h, modifiqué el índice de referencia para usar VirtualAlloc y VirtualLock. Puedo confirmar una mejora significativa en comparación con cuando no se utiliza VirtualLock. En general, Win10 sigue siendo 30% a 40% más lento que Win7 cuando ambos usan VirtualAlloc y VirtualLock.


Desafortunadamente no es una respuesta, solo una idea adicional.

Pequeño experimento con una estrategia de asignación diferente:

#include <Windows.h> #include <thread> #include <condition_variable> #include <mutex> #include <queue> #include <atomic> #include <iostream> #include <chrono> class AllocTest { public: virtual void* Alloc(size_t size) = 0; virtual void Free(void* allocation) = 0; }; class BasicAlloc : public AllocTest { public: void* Alloc(size_t size) override { return VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); } void Free(void* allocation) override { VirtualFree(allocation, NULL, MEM_RELEASE); } }; class ThreadAlloc : public AllocTest { public: ThreadAlloc() { t = std::thread([this]() { std::unique_lock<std::mutex> qlock(this->qm); do { this->qcv.wait(qlock, [this]() { return shutdown || !q.empty(); }); { std::unique_lock<std::mutex> rlock(this->rm); while (!q.empty()) { q.front()(); q.pop(); } } rcv.notify_all(); } while (!shutdown); }); } ~ThreadAlloc() { { std::unique_lock<std::mutex> lock1(this->rm); std::unique_lock<std::mutex> lock2(this->qm); shutdown = true; } qcv.notify_all(); rcv.notify_all(); t.join(); } void* Alloc(size_t size) override { void* target = nullptr; { std::unique_lock<std::mutex> lock(this->qm); q.emplace([this, &target, size]() { target = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); VirtualLock(target, size); VirtualUnlock(target, size); }); } qcv.notify_one(); { std::unique_lock<std::mutex> lock(this->rm); rcv.wait(lock, [&target]() { return target != nullptr; }); } return target; } void Free(void* allocation) override { { std::unique_lock<std::mutex> lock(this->qm); q.emplace([allocation]() { VirtualFree(allocation, NULL, MEM_RELEASE); }); } qcv.notify_one(); } private: std::queue<std::function<void()>> q; std::condition_variable qcv; std::condition_variable rcv; std::mutex qm; std::mutex rm; std::thread t; std::atomic_bool shutdown = false; }; int main() { SetProcessWorkingSetSize(GetCurrentProcess(), size_t(4) * 1024 * 1024 * 1024, size_t(16) * 1024 * 1024 * 1024); BasicAlloc alloc1; ThreadAlloc alloc2; AllocTest *allocator = &alloc2; const size_t buffer_size =1*1024*1024; const size_t buffer_count = 10*1024; const unsigned int thread_count = 32; std::vector<void*> buffers; buffers.resize(buffer_count); std::vector<std::thread> threads; threads.resize(thread_count); void* reference = allocator->Alloc(buffer_size); std::memset(reference, 0xaa, buffer_size); auto func = [&buffers, allocator, buffer_size, buffer_count, reference, thread_count](int thread_id) { for (int i = thread_id; i < buffer_count; i+= thread_count) { buffers[i] = allocator->Alloc(buffer_size); std::memcpy(buffers[i], reference, buffer_size); allocator->Free(buffers[i]); } }; for (int i = 0; i < 10; i++) { std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now(); for (int t = 0; t < thread_count; t++) { threads[t] = std::thread(func, t); } for (int t = 0; t < thread_count; t++) { threads[t].join(); } std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count(); std::cout << duration << std::endl; } DebugBreak(); return 0; }

En todas las condiciones, BasicAlloc es más rápido, como debería ser. De hecho, en una CPU de cuatro núcleos (sin HT), no hay ninguna constelación en la que ThreadAlloc pueda superarla. ThreadAlloc es constantemente un 30% más lento. (¡Lo cual es sorprendentemente pequeño, y se mantiene verdadero incluso para pequeñas asignaciones de 1kB!)

Sin embargo, si la CPU tiene alrededor de 8-12 núcleos virtuales, eventualmente llega al punto en que BasicAlloc realmente escala negativamente, mientras que ThreadAlloc simplemente "se detiene" en la línea base de fallas blandas.

Si perfila las dos estrategias de asignación diferentes, puede ver que para un bajo conteo de subprocesos, KiPageFault cambia de memcpy en BasicAlloc a VirtualLock en ThreadAlloc .

Para mayores recuentos de subprocesos y núcleos, eventualmente ExpWaitForSpinLockExclusiveAndAcquire comienza a emerger prácticamente sin carga hasta un 50% con BasicAlloc , mientras que ThreadAlloc solo mantiene la sobrecarga constante de KiPageFault .

Bueno, el puesto con ThreadAlloc también es bastante malo. No importa cuántos núcleos o nodos en un sistema NUMA tenga, actualmente tiene un límite máximo de alrededor de 5-8GB / s en nuevas asignaciones, en todos los procesos del sistema, únicamente limitado por el rendimiento de un único subproceso. Todo el hilo dedicado de administración de memoria logra, no desperdicia ciclos de CPU en una sección critica contenida.

Habría esperado que Microsoft tuviera una estrategia sin bloqueo para asignar páginas en diferentes núcleos, pero aparentemente eso no es remotamente el caso.

El bloqueo de giro también ya estaba presente en las implementaciones de Windows 7 y anteriores de KiPageFault . Entonces, ¿qué cambió?

Respuesta simple: KiPageFault se volvió mucho más lento. No hay idea de qué fue exactamente lo que causó la desaceleración, pero el bloqueo de giro simplemente nunca se convirtió en un límite obvio, porque el 100% de la contención nunca antes fue posible.

Si alguien desea desarmar KiPageFault para encontrar la parte más cara, sea mi invitado.


Microsoft parece haber solucionado este problema con Windows 10 Fall Creators Update y Windows 10 Pro for Workstation.

Aquí está el gráfico actualizado.

Win 10 FCU y WKS tienen una sobrecarga menor que Win 7. A cambio, VirtualLock parece tener una sobrecarga superior.