29 votes

Performances médiocres de Windows 10 par rapport à Windows 7 (la gestion des erreurs de page n'est pas évolutive, conflit de verrouillage grave en l'absence de threads> 16)

Nous avons mis en place deux identiques HP Z840 des Postes de travail avec les spécifications suivantes

  • 2 x Xeon E5-2690 v4 @ 2.60 GHz (Turbo Boost SUR, HT OFF, total 28 Processeurs logiques)
  • 32GB DDR4 2400 Mémoire, Quad-channel

et installé Windows 7 SP1 (x64), Windows 10 Créateurs de mise à Jour (x64) sur chaque.

Ensuite, nous avons fait un petit mémoire de référence (code ci-dessous, construit avec VS2015 mise à Jour 3, l'architecture 64 bits) qui effectue l'allocation de la mémoire-remplissage-gratuit simultanément à partir de plusieurs threads.

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

Étonnamment, le résultat est très mauvais dans Windows 10 CU par rapport à Windows 7. J'ai tracé le résultat ci-dessous pour la taille de bloc de 1 mo et 8 mo de taille de bloc, en variant le nombre de threads de 2,4,.., jusqu'à 28. Alors que Windows 7 a donné un peu moins bonne performance lorsque nous avons augmenté le nombre de threads, Windows 10 a donné bien pire évolutivité.

Windows 10 memory access is not scalable

Nous avons essayé de assurez-vous que toutes les Fenêtres de mise à jour est appliquée, la mise à jour de pilotes, d'ajuster les paramètres du BIOS, sans succès. Nous avons également effectué le même test sur plusieurs autres plates-formes matérielles, et toutes ont donné courbe similaire pour Windows 10. Il semble donc y avoir un problème de Windows 10.

Quelqu'un aurait-il une expérience similaire, ou peut-être de savoir-faire sur ce sujet (peut-être que nous avons raté quelque chose ?). Ce comportement a fait de notre application multithread ai significative des performances.

*** ÉDITÉ

À l'aide de https://github.com/google/UIforETW (merci à Bruce Dawson) pour analyser l'indice de référence, nous avons constaté que la plupart du temps est passé à l'intérieur du grain KiPageFault. Creuser plus loin en bas de l'arbre d'appel, conduit à tous les ExpWaitForSpinLockExclusiveAndAcquire. Semble que le verrouillage est à l'origine de ce problème.

enter image description here

*** ÉDITÉ

Recueillies Server 2012 R2 de données sur le même matériel. Server 2012 R2 est aussi pire que Win7, mais toujours beaucoup mieux que Win10 CU.

enter image description here

*** ÉDITÉ

Il arrive dans le Serveur de 2016 ainsi. J'ai ajouté la balise windows-server-2016.

*** ÉDITÉ

À l'aide de info de @Ext3h, j'ai modifié l'indice de référence à utiliser VirtualAlloc et VirtualLock. Je peux confirmé une amélioration significative par rapport à quand VirtualLock n'est pas utilisé. Ensemble Win10 est toujours de 30% à 40% plus lent que Win7 lorsque les deux à l'aide de VirtualAlloc et VirtualLock.

enter image description here

9voto

nikoniko Points 503

Microsoft semble avoir résolu ce problème avec Windows 10 Fall Creators Update et Windows 10 Pro for Workstation.

Voici le graphique mis à jour.

entrez la description de l'image ici

Win 10 FCU et WKS ont des frais généraux inférieurs à Win 7. En échange, le VirtualLock semble avoir des frais généraux plus élevés.

4voto

Ext3h Points 2776

Malheureusement pas une réponse, juste une idée supplémentaire.

Petite expérience avec une répartition différente de la stratégie:

#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 vertu de tous les sane conditions, BasicAlloc est plus rapide, juste ce qu'il faut. En fait, sur un PROCESSEUR quad core (pas de HT), il n'y a pas de constellation dans laquelle ThreadAlloc pourrait surpasser cela. ThreadAlloc est constamment autour de 30% plus lent. (Qui est en fait étonnamment peu, et il ne cesse de vrai même pour les petits de 1 ko dotations!)

Cependant, si le CPU a autour de 8 à 12 cœurs virtuels, puis elle finit par atteindre le point où BasicAlloc fait échelles négativement, alors que ThreadAlloc seulement "stands" sur la ligne de base généraux de doux défauts.

Si vous profil les deux différentes stratégies de répartition de l', vous pouvez voir que pour un faible nombre de threads, KiPageFault des quarts de travail de memcpy sur BasicAlloc de VirtualLock sur ThreadAlloc.

Pour plus élevé de fil et le nombre de cœurs, finalement ExpWaitForSpinLockExclusiveAndAcquire commence émergents à partir de pratiquement zéro charge jusqu'à 50% avec BasicAlloc, tandis que l' ThreadAlloc seulement maintient les frais généraux constants de KiPageFault lui-même.

Ainsi, le stand avec l' ThreadAlloc est également assez mauvais. N'importe comment beaucoup de cœurs ou des nœuds dans un système NUMA vous avez, vous êtes actuellement difficile plafonné à environ 5-8 GO/s dans de nouvelles attributions, à travers tous les processus du système, uniquement limité par un seul thread de performance. Toute la mémoire dédiée de gestion des thread qui en découle, est de ne pas gaspiller des cycles de PROCESSEUR sur un soutenu section critique.

Vous s'attendre à ce que Microsoft avait un lock gratuit de stratégie pour l'attribution de pages sur les différents cœurs, mais apparemment ce n'est pas même à distance le cas.


Le spin-lock a également déjà présent dans Windows 7 et les versions antérieures des implémentations KiPageFault. Donc ce qui a changé?

Réponse Simple: KiPageFault lui-même est devenu beaucoup plus lent. Aucune idée de ce qu'est exactement l'origine de ce ralentissement, mais le spin-lock simplement ne s'est jamais évident limite, parce que 100% de la contention n'a jamais été possible auparavant.

Si quelqu'un prétend démonter KiPageFault de trouver la partie la plus coûteuse - être mon invité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X