【问题标题】:Multi-threading performance much worse on Windows 10 than LinuxWindows 10 上的多线程性能比 Linux 差很多
【发布时间】:2018-12-15 11:19:00
【问题描述】:

我将一个多线程 Linux 应用程序移植到 Windows,并在运行 Windows 10 Pro 的服务器上对其进行测试。与在相同双引导硬件上运行的 Linux 版本的性能相比,Windows 版本的性能非常糟糕。我将代码简化为一个显示相同症状的小型多线程示例。我希望 SO 社区能够提供一些见解,了解为什么 Windows 和 Linux 之间在此应用程序上存在如此大的性能差异,并就如何解决该问题提出建议。

我正在测试的机器具有双 Intel Xeon Gold 6136 CPU(24/48 个物理/逻辑内核)@3.0 GHz(涡轮增压至 3.6 GHz)和 128 GB 内存。机器设置为双启动 CentOS 或 Windows 10。没有运行 Windows Hypervisor(Hyper-V 已禁用)。 NUMA 被禁用。在我正在执行的测试中,每个线程都应该能够在单独的核心上运行;没有其他消耗处理器的应用程序正在运行。

应用程序执行复杂的转换,将约 15 MB 的输入数据集转换为约 50 MB 的输出数据。我编写了简化的多线程测试(仅计算、仅数据移动等)以缩小问题范围。仅计算测试显示没有性能差异,但数据复制方案确实如此。可重复的场景是让每个线程将数据从其 15 MB 输入缓冲区复制到其 50 MB 输出缓冲区。输入缓冲区中的每个“int”连续写入输出缓冲区 3 次。下面显示了几乎相同的 Linux 和 Windows 代码在 N 线程的 100 次迭代中的结果:

          Windows (or cygwin)        Linux (native)
Threads   Time (msec)                Time (msec)
1         4200                       3000
2         4020                       2300
3         4815                       2300
4         6700                       2300
5         8900                       2300
6         14000                      2300
7         16500                      2300
8         21000                      2300
12        39000                      2500
16        75000                      3000
24        155000                     4000

以上时间是工作线程中的处理时间。结果不包括分配内存或启动线程的任何时间。看来Linux下线程是独立运行的,而Windows 10下不是。

我用于 Windows 测试的完整 C 代码在这里:

//
// Thread test program
//
// To compile for Windows:
//      vcvars64.bat
//      cl /Ox -o windowsThreadTest windowsThreadTest.c
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <windows.h>
#include <process.h>

#define __func__ __FUNCTION__

//
// Global data
//
HANDLE *threadHandleArray = NULL;
DWORD *threadIdArray = NULL;

//
// Time keeping
//
double *PCFreq = NULL;
__int64 *CounterStart = NULL;

void StartCounter(int whichProcessor)
{
    LARGE_INTEGER li;
    DWORD_PTR old_mask;

    if ( !PCFreq )
    {
        printf("No freq array\n");
        return;
    }

    if(!QueryPerformanceFrequency(&li))
    {
        printf("QueryPerformanceFrequency failed!\n");
        return;
    }

    PCFreq[whichProcessor] = ((double)(li.QuadPart))/1000.0;

    QueryPerformanceCounter(&li);
    CounterStart[whichProcessor] = li.QuadPart;

}

double GetCounter()
{
    LARGE_INTEGER li;
    DWORD_PTR old_mask;
    DWORD whichProcessor;
    whichProcessor = GetCurrentProcessorNumber();

    if ( CounterStart && CounterStart[whichProcessor] != 0 )
    {
        QueryPerformanceCounter(&li);
        return ((double)(li.QuadPart-CounterStart[whichProcessor]))/PCFreq[whichProcessor];
    }
    else
        return 0.0;
}


typedef struct
{
    int retVal;
    int instance;
    long myTid;
    int verbose;
    double startTime;
    double elapsedTime;
    double totalElapsedTime;
    struct {
        unsigned intsToCopy;
        int *inData;
        int *outData;
    } rwInfo;
} info_t;

int rwtest( unsigned intsToCopy, int *inData, int *outData)
{
    unsigned i, j;

    //
    // Test is simple.  For every entry in input array, write 3 entries to output
    //
    for ( j = i = 0; i < intsToCopy; i++ )
    {
        outData[j] = inData[i];
        outData[j+1] = inData[i];
        outData[j+2] = inData[i];
        j += 3;
    }
    return 0;
}

DWORD WINAPI workerProc(LPVOID *workerInfoPtr)
{
    info_t *infoPtr = (info_t *)workerInfoPtr;
    infoPtr->myTid = GetCurrentThreadId();
    double endTime;
    BOOL result;

    SetThreadPriority(threadHandleArray[infoPtr->instance], THREAD_PRIORITY_HIGHEST);

    // record start time
    infoPtr->startTime = GetCounter();

    // Run the test
    infoPtr->retVal = rwtest( infoPtr->rwInfo.intsToCopy, infoPtr->rwInfo.inData, infoPtr->rwInfo.outData );

    // end time
    endTime = GetCounter();
    infoPtr->elapsedTime = endTime - infoPtr->startTime;

    if ( infoPtr->verbose )
        printf("(%04x): done\n", infoPtr->myTid);

    return 0;
}

//
// Main Test Program
//

int main(int argc, char **argv)
{

    int i, j, verbose=0, loopLimit;
    unsigned size;
    unsigned int numThreads;
    info_t *w_info = NULL;
    int numVirtualCores;
    SYSTEM_INFO sysinfo;
    GetSystemInfo(&sysinfo);

    if ( argc != 4 )
    {
        printf("windowsThreadTest <numLoops> <numThreads> <Input size in MB>\n");
        return -1;
    }

    numVirtualCores = sysinfo.dwNumberOfProcessors;
    printf("%s: There are %d processors\n", __func__, numVirtualCores);

    // Setup Timing
    PCFreq = (double *)malloc(numVirtualCores * sizeof(double));
    CounterStart = (__int64 *)malloc(numVirtualCores * sizeof(__int64));
    if (!PCFreq || !CounterStart)
        goto free_and_exit;

    for ( i = 0; i < numVirtualCores; i++)
        StartCounter(i);

    //
    // Process  input args
    //
    loopLimit = atoi( argv[1] );
    numThreads = atoi( argv[2] );
    size = atoi( argv[3] ) * 1024 * 1024;

    //
    // Setup data array for each thread
    //
    w_info = (info_t *)malloc( numThreads * sizeof(info_t) );
    if ( !w_info )
    {
        printf("Couldn't allocate w_info of size %zd, numThreads=%d\n", sizeof(info_t), numThreads);
        goto free_and_exit;
    }
    memset( w_info, 0, numThreads * sizeof(info_t) );

    //
    // Thread Handle Array
    //
    threadHandleArray = (HANDLE *)malloc( numThreads * sizeof(HANDLE) );
    if ( !threadHandleArray )
    {
        printf("Couldn't allocate handleArray\n");
        goto free_and_exit;
    }

    //
    // Thread ID Array
    //
    threadIdArray = (DWORD *)malloc( numThreads * sizeof(DWORD) );
    if ( !threadIdArray )
    {
        printf("Couldn't allocate IdArray\n");
        goto free_and_exit;
    }

    //
    // Run the test
    //
    printf("Read/write testing... threads %d loops %lu input size %u \n", numThreads, loopLimit, size);

    for ( j = 0; j < loopLimit; j++ )
    {
        //
        // Set up the data for the threads
        //
        for ( i = 0; i < numThreads; i++ )
        {
            int idx;
            int *inData;
            int *outData;
            unsigned inSize;
            unsigned outSize;

            inSize = size;          // in MB
            outSize = size * 3;     // in MB

            //
            // Allocate input buffer
            //
            inData = (int *) malloc( inSize );
            if ( !inData )
            {
                printf("Error allocating inData of size %zd\n", inSize * sizeof(char));
                goto free_and_exit;
            }
            else
            {
                if ( verbose )
                    printf("Allocated inData of size %zd\n", inSize * sizeof(char));
            }

            //
            // Allocate output buffer 3x the size of the input buf
            //
            outData = (int *) malloc( outSize * 3 );
            if ( !outData )
            {
                printf("Error allocating outData of size %zd\n", outSize * sizeof(char));
                goto free_and_exit;
            }
            else
            {
                if ( verbose )
                    printf("Allocated outData of size %zd\n", outSize * sizeof(char));
            }

            //
            // Put some data into input buffer
            //
            w_info[i].rwInfo.intsToCopy = inSize/sizeof(int);

            for ( idx = 0; idx < w_info[i].rwInfo.intsToCopy; idx++)
                inData[idx] = idx;

            w_info[i].rwInfo.inData = inData;
            w_info[i].rwInfo.outData = outData;

            w_info[i].verbose = verbose;
            w_info[i].instance = i;
            w_info[i].retVal = -1;
        }

        //
        // Start the threads
        //
        for ( i = 0; i < numThreads; i++ )
        {
            threadHandleArray[i] = CreateThread( NULL, 0, workerProc, &w_info[i], 0, &threadIdArray[i] );
            if ( threadHandleArray[i] == NULL )
            {
                fprintf(stderr, "Error creating thread %d\n", i);
                return 1;
            }
        }

        //
        // Wait until all threads have terminated.
        //
        WaitForMultipleObjects( numThreads, threadHandleArray, TRUE, INFINITE );

        //
        // Check the return values
        //
        for ( i = 0; i < numThreads; i++ )
        {
            if ( w_info[i].retVal < 0 )
            {
                printf("Error return from thread %d\n", i);
                goto free_and_exit;
            }
            if ( verbose )
                printf("Thread %d, tid %x %f msec\n", i, (unsigned)w_info[i].myTid, w_info[i].elapsedTime);
            w_info[i].totalElapsedTime += w_info[i].elapsedTime;
        }

        //
        // Free up the data from this iteration
        //
        for ( i = 0; i < numThreads; i++ )
        {
            free( w_info[i].rwInfo.inData );
            free( w_info[i].rwInfo.outData );
            CloseHandle( threadHandleArray[i] );
        }
    }

    //
    // All done, print out cumulative time spent in worker routine
    //
    for ( i = 0; i < numThreads; i++ )
    {
        printf("Thread %d, loops %d %f msec\n", i, j, w_info[i].totalElapsedTime);
    }

free_and_exit:

    if ( threadHandleArray )
        free( threadHandleArray );

    if ( threadIdArray )
        free( threadIdArray );

    if ( PCFreq )
        free( PCFreq );

    if ( CounterStart )
        free( CounterStart );

    if ( w_info )
        free( w_info );

    return 0;
}

上面的代码很容易更改为使用 pthreads,使用命令行 'gcc -O3 -o pthreadTestLinux pthreadTest.c' 进行编译以获得上述 Linux 结果(如果需要,我可以发布)。如果在 cygwin 环境中使用 gcc 在 Windows 上编译,结果与使用 Windows 示例代码的结果相同。

我尝试了各种 BIOS 设置、提高线程优先级、预分配线程池等,但性能没有任何变化。我不认为这是 错误共享 的情况,因为 Linux 版本使用几乎相同的代码显示完全不同的性能。我想知道我的编译方式是否有问题。我正在使用 64 位工具链。

有什么想法吗?

【问题讨论】:

  • 我会先尝试使用“本机”编译器来确保,尽管 Cygwin 所做的任何事情都不应该影响这种代码,但是......
  • 也许使用SetThreadAffinityMask / GetThreadIdealProcessorEx 会有所帮助?
  • 您在任务管理器性能选项卡中看到多少 CPU 图表?
  • 您上面显示的时间与程序的输出不同。它们有什么关系?
  • 那里有很多代码,很可能你正在 Windows 环境中做一些“Linux-y”......举个简单的例子,你不必要地为每个处理器调用QueryPerformanceFrequency:“ ...只需要在应用初始化时查询频率,并且可以缓存结果" ...移植通常意味着为您的目标平台翻译代码,而不是代码的音译..就像你把日语翻译成英语一样,这没有意义......

标签: c linux windows multithreading cygwin


【解决方案1】:

我在多核/多处理器机器上看到了 Cygwin 应用程序的类似问题。据我所知,这在 Cygwin 中仍然是一个未解决的问题。

我注意到并且您可以尝试的一件事是,将进程固定到单个 CPU 可能会显着提高其性能(但显然也会限制利用多核和多线程并行性的能力)。您可以使用 Windows 任务管理器将进程关联设置为仅一个 CPU/内核,从而将进程固定到单个 CPU。

如果这样做可以显着提高单个线程的性能,那么您会看到与我注意到的相同的问题。而且,我认为这不是您的代码的问题,而是 Cygwin 的问题。

【讨论】:

    【解决方案2】:

    很想知道在 golang 中的多线程内存转换问题上,Windows 性能与 Linux 性能相比如何,因此我将代码移植到尽可能接近原始代码,然后做了一些相同的操作在类似的硬件平台上进行性能测试。

    与发布的问题中看到的结果不同,golang 代码并没有随着同时操作数量的增加而崩溃。对应的性能图是:

    Num Threads      Time in Process
        1                 4000
        2                 4100
        4                 4200
        6                 3600
        12                3600
        16                3800
        24                3700
    

    这些结果比您在 Linux 上运行的 C 代码中显示的要慢得多。

    不确定这是否有帮助,但看起来 Windows 10 存在一个普遍问题,在执行一些内存操作时会导致多线程性能问题,而且似乎与 C 的性能有关如您在问题中所述,由 cl 和 gcc (cygwin) 编译时的代码。

    golang代码是:

    package main
    
    import "fmt"
    import "os"
    import "time"
    import "strconv"
    
    
    func rwtest(intsToCopy int, inData *[]int, outData *[]int) {
        var i int
        var j int
    
        j = 0
    
        for i=0 ; i<intsToCopy ; i++ {
            (*outData)[j + 0] = (*inData)[i]
            (*outData)[j + 1] = (*inData)[i]
            (*outData)[j + 2] = (*inData)[i]
    
            j += 3
        }
    }
    
    
    func workerProc(threadNum int, reportChan chan int, numLoops int, dataSize int) {
        var i int
        var inData []int
        var outData []int
        var cumulativeTime time.Duration
    
        cumulativeTime = 0
    
        for i=0 ; i<numLoops ; i++ {
            inData = make([]int, dataSize, dataSize)
            outData = make([]int, dataSize * 3, dataSize * 3)
    
            startTime := time.Now()
    
            rwtest(dataSize, &inData, &outData)
    
            endTime := time.Now()
    
            cumulativeTime += endTime.Sub(startTime)
    
            inData = nil
            outData = nil
        }
    
        // Print out the cumulative time
        fmt.Printf("Thread %d duration is %d\n", threadNum, cumulativeTime)
    
        // Write out to the channel
        reportChan <- 0
    
    }
    
    
    func main() {
        var i int
    
        if len(os.Args) != 4 {
            fmt.Printf("Usage: %s <num threads> <num loops> <data size>\n", os.Args[0])
    
            return
        }
    
        numThreads, _ := strconv.Atoi(os.Args[1])
        numLoops, _ := strconv.Atoi(os.Args[2])
        dataSize, _ := strconv.Atoi(os.Args[3])
    
        fmt.Printf("Running Program with %d threads, with %d loops\n", numThreads, numLoops)
    
        // Make a channel for each thread
        var chans []chan int
    
        for i=0 ; i<numThreads ; i++ {
            chans = append(chans, make(chan int))
        }
    
        // start the threads
        for i=0 ; i<numThreads ; i++ {
            go workerProc(i, chans[i], numLoops, dataSize)
        }
    
        var x int
    
        // Loop through the channels, waiting for each go routine to finish
        for i=0 ; i<numThreads ; i++ {
            x = <-chans[i]
        }
    
        fmt.Printf("Done: %d\n", x)
    }
    

    【讨论】:

      【解决方案3】:

      Youtubers Level 1 Techs 也在 Threadripper 处理器上看到了这一点。长话短说,当程序运行时,Windows 10 内核似乎在 FAR FAR 内核之间改组线程。 https://www.youtube.com/watch?v=M2LOMTpCtLA

      我不知道这是否也是 Server 2016 或 2019 内核的问题。作为 Threadripper 2950x 的新主人,我真的很想解决这个问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-10-07
        • 2014-10-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-27
        • 1970-01-01
        相关资源
        最近更新 更多