与Windows 7相比,Windows 10性能差(页面故障处理不可扩展,当线程数> 16时存在严重的锁竞争)。

30

我们搭建了两台完全相同的HP Z840工作站,规格如下:

  • 2 x Xeon E5-2690 v4 @ 2.60GHz (Turbo Boost 开启,超线程关闭,总共28个逻辑处理器)
  • 32GB DDR4 2400内存,四通道

并在每台机器上安装了Windows 7 SP1 (x64) 和Windows 10 Creators Update (x64)。

然后我们运行了一个小型的内存基准测试(以下是代码),该测试同时从多个线程中执行内存分配、填充和释放操作。

#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相比非常糟糕。 我绘制了以下1MB块大小和8MB块大小的结果,将线程数从2,4,..变化到28。 当我们增加线程数时,虽然Windows 7的性能略微差一些,但Windows 10的可扩展性要差得多。

Windows 10 memory access is not scalable

我们已经尝试确保应用了所有Windows更新、更新了驱动程序、调整了BIOS设置,但都没有成功。我们还在几个其他硬件平台上运行了相同的基准测试,并且所有的测试结果表明Windows 10的曲线类似。因此,这似乎是Windows 10的问题。

是否有人有类似的经验,或者可能了解这个问题(也许我们遗漏了什么?)。这种行为使我们的多线程应用程序受到了显着的性能损失。

***编辑

使用https://github.com/google/UIforETW(感谢Bruce Dawson)分析基准测试,我们发现大部分时间都花在内核的KiPageFault中。进一步钻下调用树,所有的线索都指向ExpWaitForSpinLockExclusiveAndAcquire。看来锁争用导致了这个问题。

enter image description here

***编辑

在相同的硬件上收集Server 2012 R2数据。 Server 2012 R2也不如Win7,但比Win10 CU好得多。

enter image description here

***编辑

这也发生在Server 2016中。我添加了标签windows-server-2016。

***编辑

使用@Ext3h提供的信息,我修改了基准测试以使用VirtualAlloc和VirtualLock。我可以证实与不使用VirtualLock时相比,有显着的改进。总体而言,当使用VirtualAlloc和VirtualLock时,Win10仍然比Win7慢30%至40%。

enter image description here


1
与微软支持联系。这是一个已知的问题,存在解决方案。但似乎还没有公开。Virtualalloc 存在性能问题。 - Alois Kraus
1
对于任何在本地测试此代码的人,请确保编译为64位。 - selbie
1
太有趣了。更多信息可能会有所帮助。特别是,额外的成本是来自分配内存(VirtualAlloc),从填充内存(页错误)还是来自释放它(取消映射页面)?这些成本可以分别测量。参见以下示例以了解这些隐藏成本: https://randomascii.wordpress.com/2014/12/10/hidden-costs-of-memory-allocation/ - Bruce Dawson
1
你尝试过最新的Win10 Insider Build 16237吗?它还有这个问题吗? - magicandre1981
1
@AloisKraus 谢谢。我们使用了创作者更新并已经应用了所有更新(我今天重新检查了一遍)。问题仍然存在。我们也测试了Insider Build 16232。 - nikoniko
显示剩余27条评论
2个回答

9
微软似乎已经通过Windows 10秋季创作者更新和Windows 10专业工作站修复了这个问题。以下是更新后的图表:enter image description here Win 10 FCU和WKS的开销比Win 7要低。但换来的是VirtualLock的开销较高。

1
看起来他们已经修复了,但是没有告诉很多人。目前从支持团队那里得到一个最终答案,以确定一个已经修复的问题是否属于我安装的这个或那个操作系统版本,还是相当困难的。 - Alois Kraus
1
一样的情况。这不是我的微软联系人告诉我的。他们仍在告诉我,他们正在确定这个问题是否是一个错误。 - nikoniko
2
感谢您告诉我们他们终于修复了它。这就是我讨厌Windows 10快速发布计划和缺失文档的原因。 - magicandre1981
还有其他版本的修复程序可用:https://support.microsoft.com/help/4096236/description-of-the-security-only-update-for-net-framework-4-6-4-6-1-4 - Rolf Kristensen

4

很遗憾这不是一个答案,只是一些额外的见解。

使用不同的分配策略进行小实验:

#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 更快,这就是它应该的表现。事实上,在一个四核CPU(没有超线程)上,不存在任何情况 ThreadAlloc 能够超越 BasicAlloc。相比之下,ThreadAlloc 慢了约 30%。(令人惊讶的是,即使对于只有 1KB 的小型分配,它也始终如一地表现出较少的性能差距!)
然而,如果 CPU 大约有 8-12 个虚拟核心,则最终会达到基准点,此时 BasicAlloc 实际上会出现负向扩展,而 ThreadAlloc 只会“停滞”在软故障的基本开销上。
如果您分析两种不同的分配策略,您会发现对于低线程计数,KiPageFaultBasicAlloc 上从 memcpy 切换到 VirtualLock
对于更高的线程和核心计数,最终 ExpWaitForSpinLockExclusiveAndAcquire 开始从几乎零负载中出现,以至于在 BasicAlloc 中可达到 50%,而 ThreadAlloc 仅保持 KiPageFault 本身的恒定开销。
嗯,ThreadAlloc 的停顿也相当严重。无论您在 NUMA 系统中有多少核心或节点,您当前只能以约 5-8GB/s 的速度进行新分配,所有进程在系统中都受到限制,仅受单线程性能限制。专用内存管理线程所实现的唯一效果是不浪费 CPU 周期来争夺关键部分。
您本应该预计 Microsoft 有一种无锁策略来为不同的核心分配页面,但显然根本不是这种情况。
旋转锁已经存在于 Windows 7 和之前的 KiPageFault 实现中。那么有什么变化吗?
简单的答案是: KiPageFault 本身变得更慢了。不知道具体是什么原因导致其放慢了,但自从以前没有 100% 的争用之后,旋转锁就根本没有成为明显的限制。
如果有人想反汇编 KiPageFault 找到最昂贵的部分,请随便使用。

谢谢,我按照你的建议使用VirtualLock重新获取了数据并解决了问题。 - nikoniko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接