Низкая производительность Windows 10 по сравнению с Windows 7 (обработка ошибок страниц не масштабируется, серьезный конфликт блокировок при отсутствии потоков > 16)

Мы установили две идентичные рабочие станции HP Z840 со следующими характеристиками.

  • 2 x Xeon E5-2690 v4 @ 2,60 ГГц (Turbo Boost ON, HT OFF, всего 28 логических процессоров)
  • 32 ГБ памяти DDR4 2400, четырехканальная

и установил Windows 7 SP1 (x64) и Windows 10 Creators Update (x64) на каждом.

Затем мы запустили небольшой тест памяти (код ниже, созданный с помощью VS2015 Update 3, 64-разрядная архитектура), который выполняет выделение памяти без заполнения одновременно из нескольких потоков.

#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;
}

Удивительно, но результат в Windows 10 CU очень плохой по сравнению с Windows 7. Я нарисовал результат ниже для размера фрагмента 1 МБ и размера фрагмента 8 МБ, варьируя количество потоков от 2, 4 до 28. В то время как Windows 7 дала немного худшую производительность, когда мы увеличили количество потоков, Windows 10 дала гораздо худшую масштабируемость.

«Доступ

Мы пытались убедиться, что применяются все обновления Windows, обновить драйверы, настроить параметры BIOS, но безуспешно. Мы также провели тот же тест на нескольких других аппаратных платформах, и все они показали одинаковую кривую для Windows 10. Так что, похоже, это проблема Windows 10.

Есть ли у кого-нибудь подобный опыт, или, может быть, ноу-хау по этому поводу (может быть, мы что-то упустили?). Такое поведение привело к значительному снижению производительности нашего многопоточного приложения.

*** ОТРЕДАКТИРОВАНО

Используя https://github.com/google/UIforETW (спасибо Брюсу Доусону) для анализа теста, мы обнаружили, что большую часть времени KiPageFault проводит внутри ядер. Если копнуть дальше по дереву вызовов, все приведет к ExpWaitForSpinLockExclusiveAndAcquire. Кажется, что конфликт блокировки вызывает эту проблему.

введите описание изображения здесь

*** ОТРЕДАКТИРОВАНО

Собраны данные Server 2012 R2 на том же оборудовании. Server 2012 R2 также хуже, чем Win7, но все же намного лучше, чем Win10 CU.

введите описание изображения здесь

*** ОТРЕДАКТИРОВАНО

Это происходит и в Server 2016. Я добавил тег windows-server-2016.

*** ОТРЕДАКТИРОВАНО

Используя информацию из @Ext3h, я изменил тест, чтобы использовать VirtualAlloc и VirtualLock. Я могу подтвердить значительное улучшение по сравнению с тем, когда VirtualLock не используется. В целом Win10 по-прежнему на 30-40% медленнее, чем Win7, при использовании VirtualAlloc и VirtualLock.

введите описание изображения здесь


person nikoniko    schedule 11.07.2017    source источник
comment
Свяжитесь со службой поддержки MS. Это известная проблема, и существует исправление. Но, похоже, оно еще не опубликовано. Virtualalloc имеет проблемы с производительностью.   -  person Alois Kraus    schedule 11.07.2017
comment
Для тех, кто тестирует этот код локально - убедитесь, что вы компилируете его как 64-битный.   -  person selbie    schedule 11.07.2017
comment
Это увлекательно. Дополнительная информация может быть полезной. В частности, это дополнительные затраты на выделение памяти (VirtualAlloc), на заполнение памяти (сбой страниц) или на ее освобождение (неотображение страниц). Эти затраты могут быть измерены отдельно. См. пример этих скрытых затрат: randomascii .wordpress.com/2014/12/10/   -  person Bruce Dawson    schedule 11.07.2017
comment
Вы пробовали последнюю версию Win10 Insider Build 16237? У него все еще есть проблема?   -  person magicandre1981    schedule 11.07.2017
comment
@BruceDawson Это первое заполнение памяти (ошибка страниц), вызывающее такое поведение. Накладные расходы malloc также в несколько раз выше в Win10, но все еще незначительны по сравнению с бесплатными затратами и затратами на ошибку страницы.   -  person nikoniko    schedule 12.07.2017
comment
@magicandre1981 Пробовал 16232, но производительность не улучшилась. А вот 16237 не пробовал.   -  person nikoniko    schedule 12.07.2017
comment
хорошо, вы тестировали Windows 8.1? Вы тоже видите это здесь? Также попробуйте отключить новое сжатие памяти Win10 и повторно запустите тесты.   -  person magicandre1981    schedule 12.07.2017
comment
@AloisKraus Я связался со службой поддержки MS, но, похоже, они не смогли найти упомянутое вами исправление. Не могли бы вы дать более подробную информацию, которую я могу отправить им?   -  person nikoniko    schedule 13.07.2017
comment
@ magicandre1981 Мы пытались отключить сжатие памяти, но безрезультатно. Среда тестирования имеет достаточно большой объем памяти (32 ГБ), поэтому сжатие не запускалось. Win 8.1 еще не пробовал.   -  person nikoniko    schedule 13.07.2017
comment
@nikoniko: я спрошу у своего контакта, что это за дело #, чтобы вы могли сослаться на него.   -  person Alois Kraus    schedule 13.07.2017
comment
@magicandre1981 Я собрал дополнительные данные для Server 2012 R2 (который, как мне кажется, использует то же ядро, что и Win 8.1). Это хуже, чем Win7, но намного лучше, чем Win 10.   -  person nikoniko    schedule 14.07.2017
comment
server 2012 отключил объединение страниц памяти, поэтому проверьте это, запустив Get-MMAgent в Powershell, и если да, включите его с помощью Enable-MMAgent -PageCombining. Сейчас тоже хуже как в Win10? возможно, это изменение вызывает проблемы с памятью   -  person magicandre1981    schedule 14.07.2017
comment
Мне сказали, что они продолжают тестировать исправление этой проблемы. Тест выполняет VirtualAlloc, за которым следует VirtualLock.   -  person Alois Kraus    schedule 14.07.2017
comment
Исправление содержится на support.microsoft.com/en. -нас/помощь/4025339/. Пожалуйста, попробуйте.   -  person Alois Kraus    schedule 17.07.2017
comment
@AloisKraus KB4025339 предназначен для Win10 v1607 (юбилейное обновление), он использует v1703 (обновление для авторов)   -  person magicandre1981    schedule 17.07.2017
comment
@magicandre1981 Вызвал Enable-MMAgent -PageCombining и перезагрузил компьютер, прежде чем вспомнить данные. К сожалению, существенных изменений нет.   -  person nikoniko    schedule 18.07.2017
comment
@AloisKraus Спасибо. Мы использовали Creators Update и уже применили все обновления (сегодня перепроверил). Тем не менее проблема сохраняется. Мы также протестировали Insider Build 16232.   -  person nikoniko    schedule 18.07.2017
comment
@nikoniko: Жаль это слышать. Похоже, вы нашли другую проблему. Исправленная проблема наблюдалась на машинах с 1 ТБ ОЗУ. Вы должны открыть вопрос в MS. Я спрошу, какой пакет патчей для Creators Update.   -  person Alois Kraus    schedule 18.07.2017
comment
@AloisKraus Да, я уже на связи с MS. Но никаких действий с их стороны пока не предпринимается.   -  person nikoniko    schedule 25.07.2017
comment
2016 основан на кодовой базе WIn10, поэтому неудивительно, что вы получили результат   -  person magicandre1981    schedule 29.07.2017
comment
Я могу добавить к этому, что он НЕ ограничен системами с несколькими сокетами. То же самое соперничество также возникало у меня в системе с одним сокетом: чтобы нетерпеливо зафиксировать выделенную память в c">stackoverflow.com/questions/45242210/ Ни один из флагов MMAgent не работал, работала только комбинация VirtualLock/VirtualUnlock в выделенном потоке программной ошибки.   -  person Ext3h    schedule 30.07.2017
comment
Вы пришли к исправлению вместе с MS?   -  person Alois Kraus    schedule 29.10.2017
comment
Нет, они постоянно говорят мне, что сообщат мне о статусе, но потом молчат, пока я их не опрошу...   -  person nikoniko    schedule 30.10.2017
comment
@nikoniko: Можете ли вы дать мне отмеченный идентификатор? Возможно, я могу сослаться на ваш билет и открыть еще один, чтобы ускорить процесс.   -  person Alois Kraus    schedule 02.11.2017
comment
Привет @Alois Kraus, идентификатор дела 117071216025275.   -  person nikoniko    schedule 06.11.2017
comment
@niko: Спасибо. Я открыл еще одно дело. Посмотрим, как получится. Я подробно описал эту проблему здесь: aloiskraus.wordpress.com/2017/11/12/, который показывает, что большая часть блокировки происходит в реализации ошибки страницы.   -  person Alois Kraus    schedule 12.11.2017
comment
@Алоис Краус, отличный текст! Извлеченные уроки: никогда не доверяйте реализации ОС управления памятью. Следует написать собственный уровень управления памятью для критичных к производительности частей.   -  person nikoniko    schedule 14.11.2017
comment
@Alois Kraus, кстати, вчера у меня была возможность использовать Win 10 Fall Creators Update и Win 10 Pro для рабочей станции. Я взял данные, и, к моему удивлению, проблема устранена! Я помещаю данные ниже (как ответ на этот вопрос).   -  person nikoniko    schedule 14.11.2017
comment
@nikoniko: Похоже, исправление уже было запущено, но служба поддержки MS не смогла сказать, в какой версии оно наконец появилось. Возможно, он не прошел через финальное тестирование сборки. Иногда исправленные исправления появляются позже из-за проблем во внутреннем тестировании. Будет интересно еще раз протестировать цифры после обновления.   -  person Alois Kraus    schedule 14.11.2017
comment
интересная ветка! мы также отслеживаем связанную с этим проблему, наблюдаемую только на WIN10. В Audio Real Time Thread мы получаем невероятные штрафы за время доступа к памяти... мы можем получить прерывание от 10, 20 до 200 мс, возможно, из-за сбоя страницы (но наша виртуальная память отключена)... и мы также видели, что VirtualLock может улучшить ситуацию в некоторых случаях... но на данный момент у нас нет объяснений: social.msdn.microsoft.com/Forums/en- США/   -  person user258609    schedule 13.02.2018
comment
Будьте осторожны при использовании VirtualLock. Я наблюдал значительную деградацию после патчей Meltdown + Spectre.   -  person nikoniko    schedule 15.02.2018
comment
Да, но с WIN10 просят использовать VirtualLock со всей памятью, задействованной в обработке в реальном времени (имеются в виду все аудиоприложения и видеоигры). см. ветку о WIN10. Основная проблема с обработкой звука: social. msdn.microsoft.com/Forums/en-US/   -  person user258609    schedule 15.02.2018


Ответы (2)


Похоже, Microsoft исправила эту проблему в Windows 10 Fall Creators Update и Windows 10 Pro for Workstation.

Вот обновленный график.

введите описание изображения здесь

Win 10 FCU и WKS имеют меньшие накладные расходы, чем Win 7. Взамен VirtualLock, похоже, имеет более высокие накладные расходы.

person nikoniko    schedule 14.11.2017
comment
Вроде исправили, но мало кому рассказали. В настоящее время довольно сложно получить от техподдержки окончательный ответ, если уже исправленная проблема является частью той или иной сборки ОС, которую я установил. - person Alois Kraus; 14.11.2017
comment
То же самое. Это не мой контакт по MS сказал мне это. Они все еще говорят мне, что находятся в процессе определения, является ли эта проблема ошибкой или нет. - person nikoniko; 14.11.2017
comment
спасибо, что сообщили нам, что они, наконец, исправили это. вот почему я ненавижу этот быстрый график выпуска Windows 10 с отсутствующей документацией. - person magicandre1981; 14.11.2017
comment
Также есть готовое исправление для других версий: support.microsoft.com/help/4096236/ - person Rolf Kristensen; 22.05.2018

К сожалению, это не ответ, а просто дополнительная информация.

Небольшой эксперимент с другой стратегией распределения:

#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;
}

При всех вменяемых условиях BasicAlloc быстрее, как и должно быть. Фактически, на четырехъядерном процессоре (без HT) нет созвездия, в котором ThreadAlloc мог бы превзойти его. ThreadAlloc постоянно примерно на 30% медленнее. (Что на самом деле на удивление мало, и это справедливо даже для крошечных выделений в 1 КБ!)

Однако, если ЦП имеет около 8-12 виртуальных ядер, то в конечном итоге он достигает точки, когда BasicAlloc фактически масштабируется отрицательно, в то время как ThreadAlloc просто «останавливается» на базовых накладных расходах на программные сбои.

Если вы профилируете две разные стратегии распределения, вы увидите, что при малом количестве потоков KiPageFault смещается с memcpy на BasicAlloc на VirtualLock на ThreadAlloc.

Для большего числа потоков и ядер в конечном итоге ExpWaitForSpinLockExclusiveAndAcquire начинает подниматься от практически нулевой нагрузки до 50% с BasicAlloc, в то время как ThreadAlloc поддерживает только постоянные накладные расходы от самого KiPageFault.

Ну и стойло с ThreadAlloc тоже довольно нехорошо. Независимо от того, сколько ядер или узлов в системе NUMA у вас есть, в настоящее время вы жестко ограничены примерно 5-8 ГБ/с в новых выделениях для всех процессов в системе, ограничиваясь исключительно производительностью одного потока. Все, чего достигает выделенный поток управления памятью, — это не тратить циклы ЦП на спорный критический раздел.

Можно было бы ожидать, что у Microsoft есть стратегия без блокировки для назначения страниц на разные ядра, но, по-видимому, это даже отдаленно не так.


Спин-блокировка также уже присутствовала в Windows 7 и более ранних реализациях KiPageFault. Так что же изменилось?

Простой ответ: KiPageFault сам стал намного медленнее. Понятия не имею, что именно вызвало его замедление, но спин-блокировка просто никогда не становилась очевидным ограничением, потому что раньше 100% конкуренция была невозможна.

Если кто-то захочет разобрать KiPageFault, чтобы найти самую дорогую деталь - милости просим.

person Ext3h    schedule 30.07.2017
comment
Спасибо, я переснял данные с помощью VirtualLock, как вы предложили, и отредактировал вопрос. - person nikoniko; 31.07.2017