双核性能不如单核

本文关键字:单核 性能 双核 | 更新日期: 2023-09-27 18:18:43

下面的nunit测试比较了在双核机器上运行单个线程和运行两个线程的性能。具体来说,这是一台运行在四核Linux SLED主机上的VMWare双核虚拟Windows 7机,主机是戴尔Inspiron 503。

每个线程只是循环和增加2个计数器,addCounter和readCounter。这个测试最初是测试一个Queue实现,结果发现它在多核机器上的性能更差。因此,在将问题缩小到可重复的小代码时,您在这里有没有队列只有递增的变量,并且令人震惊和沮丧的是,两个线程比一个线程慢得多。

当运行第一个测试时,任务管理器显示其中一个核心100%繁忙,另一个核心几乎空闲。下面是单线程测试的测试输出:

readCounter 360687000
readCounter2 0
total readCounter 360687000
addCounter 360687000
addCounter2 0

你看到超过3.6亿个增量!

接下来,双线程测试显示在测试的整个5秒持续时间内,两个内核上都100%繁忙。然而,它的输出只显示:

readCounter 88687000
readCounter2 134606500
totoal readCounter 223293500
addCounter 88687000
addCounter2 67303250
addFailure0

只有2.23亿个读增量。这两个CPU在这5秒内做了什么,以减少工作量?

有没有可能的线索?你能在你的机器上运行测试看看是否得到不同的结果吗?一种想法是,也许VMWare的双核性能不是你所希望的。

using System;
using System.Threading;
using NUnit.Framework;
namespace TickZoom.Utilities.TickZoom.Utilities
{
    [TestFixture]
    public class ActiveMultiQueueTest
    {
        private volatile bool stopThread = false;
        private Exception threadException;
        private long addCounter;
        private long readCounter;
        private long addCounter2;
        private long readCounter2;
        private long addFailureCounter;
        [SetUp]
        public void Setup()
        {
            stopThread = false;
            addCounter = 0;
            readCounter = 0;
            addCounter2 = 0;
            readCounter2 = 0;
        }

        [Test]
        public void TestSingleCoreSpeed()
        {
            var speedThread = new Thread(SpeedTestLoop);
            speedThread.Name = "1st Core Speed Test";
            speedThread.Start();
            Thread.Sleep(5000);
            stopThread = true;
            speedThread.Join();
            if (threadException != null)
            {
                throw new Exception("Thread failed: ", threadException);
            }
            Console.Out.WriteLine("readCounter " + readCounter);
            Console.Out.WriteLine("readCounter2 " + readCounter2);
            Console.Out.WriteLine("total readCounter " + (readCounter + readCounter2));
            Console.Out.WriteLine("addCounter " + addCounter);
            Console.Out.WriteLine("addCounter2 " + addCounter2);
        }
        [Test]
        public void TestDualCoreSpeed()
        {
            var speedThread1 = new Thread(SpeedTestLoop);
            speedThread1.Name = "Speed Test 1";
            var speedThread2 = new Thread(SpeedTestLoop2);
            speedThread2.Name = "Speed Test 2";
            speedThread1.Start();
            speedThread2.Start();
            Thread.Sleep(5000);
            stopThread = true;
            speedThread1.Join();
            speedThread2.Join();
            if (threadException != null)
            {
                throw new Exception("Thread failed: ", threadException);
            }
            Console.Out.WriteLine("readCounter " + readCounter);
            Console.Out.WriteLine("readCounter2 " + readCounter2);
            Console.Out.WriteLine("totoal readCounter " + (readCounter + readCounter2));
            Console.Out.WriteLine("addCounter " + addCounter);
            Console.Out.WriteLine("addCounter2 " + addCounter2);
            Console.Out.WriteLine("addFailure" + addFailureCounter);
        }
        private void SpeedTestLoop()
        {
            try
            {
                while (!stopThread)
                {
                    for (var i = 0; i < 500; i++)
                    {
                        ++addCounter;
                    }
                    for (var i = 0; i < 500; i++)
                    {
                        readCounter++;
                    }
                }
            }
            catch (Exception ex)
            {
                threadException = ex;
            }
        }
        private void SpeedTestLoop2()
        {
            try
            {
                while (!stopThread)
                {
                    for (var i = 0; i < 500; i++)
                    {
                        ++addCounter2;
                        i++;
                    }
                    for (var i = 0; i < 500; i++)
                    {
                        readCounter2++;
                    }
                }
            }
            catch (Exception ex)
            {
                threadException = ex;
            }
        }

    }
}

编辑:我在一台没有vmware的四核笔记本电脑上测试了上面的内容,并且得到了类似的性能下降。因此,我编写了另一个类似于上面的测试,但每个线程方法在一个单独的类中。我这样做的目的是为了测试4个内核。

嗯,测试显示了出色的结果,在1,2,3或4核时几乎线性提高。

现在在两台机器上进行了一些实验,似乎只有当主线程方法在不同的实例上而不是在同一个实例上时,才会出现适当的性能。

换句话说,如果在一个特定类的相同实例上使用多个线程的主入口方法,那么对于您添加的每个线程,多核上的性能将变得更差,而不是像您想象的那样更好。

CLR似乎在"同步",所以一次只有一个线程可以在该方法上运行。然而,我的测试表明情况并非如此。所以现在还不清楚发生了什么。

但是我自己的问题似乎可以简单地通过使运行线程的方法的单独实例作为它们的起点来解决。

真诚,韦恩

编辑:

这里是一个更新的单元测试,测试1,2,3,&4个线程,它们都在同一个类的实例上。在线程循环中使用带有变量的数组至少要间隔10个元素。而且每增加一个线程,性能仍然会显著下降。

using System;
using System.Threading;
using NUnit.Framework;
namespace TickZoom.Utilities.TickZoom.Utilities
{
    [TestFixture]
    public class MultiCoreSameClassTest
    {
        private ThreadTester threadTester;
        public class ThreadTester
        {
            private Thread[] speedThread = new Thread[400];
            private long[] addCounter = new long[400];
            private long[] readCounter = new long[400];
            private bool[] stopThread = new bool[400];
            internal Exception threadException;
            private int count;
            public ThreadTester(int count)
            {
                for( var i=0; i<speedThread.Length; i+=10)
                {
                    speedThread[i] = new Thread(SpeedTestLoop);
                }
                this.count = count;
            }
            public void Run()
            {
                for (var i = 0; i < count*10; i+=10)
                {
                    speedThread[i].Start(i);
                }
            }
            public void Stop()
            {
                for (var i = 0; i < stopThread.Length; i+=10 )
                {
                    stopThread[i] = true;
                }
                for (var i = 0; i < count * 10; i += 10)
                {
                    speedThread[i].Join();
                }
                if (threadException != null)
                {
                    throw new Exception("Thread failed: ", threadException);
                }
            }
            public void Output()
            {
                var readSum = 0L;
                var addSum = 0L;
                for (var i = 0; i < count; i++)
                {
                    readSum += readCounter[i];
                    addSum += addCounter[i];
                }
                Console.Out.WriteLine("Thread readCounter " + readSum + ", addCounter " + addSum);
            }
            private void SpeedTestLoop(object indexarg)
            {
                var index = (int) indexarg;
                try
                {
                    while (!stopThread[index*10])
                    {
                        for (var i = 0; i < 500; i++)
                        {
                            ++addCounter[index*10];
                        }
                        for (var i = 0; i < 500; i++)
                        {
                            ++readCounter[index*10];
                        }
                    }
                }
                catch (Exception ex)
                {
                    threadException = ex;
                }
            }
        }
        [SetUp]
        public void Setup()
        {
        }

        [Test]
        public void SingleCoreTest()
        {
            TestCores(1);
        }
        [Test]
        public void DualCoreTest()
        {
            TestCores(2);
        }
        [Test]
        public void TriCoreTest()
        {
            TestCores(3);
        }
        [Test]
        public void QuadCoreTest()
        {
            TestCores(4);
        }
        public void TestCores(int numCores)
        {
            threadTester = new ThreadTester(numCores);
            threadTester.Run();
            Thread.Sleep(5000);
            threadTester.Stop();
            threadTester.Output();
        }
    }
}

双核性能不如单核

只有2.23亿个读增量。这两个CPU在这5秒内做了什么,以减少工作量?

您可能会遇到缓存争用—当单个CPU增加您的整数时,它可以在自己的L1缓存中这样做,但是一旦两个CPU开始"争夺"相同的值,每次每个CPU访问它时,它所在的缓存行必须在它们的缓存之间来回复制。在缓存之间复制数据所花费的额外时间加起来,特别是当您正在执行的操作(增加整数)如此微不足道时。

几点:

  1. 你应该测试每个设置至少10次,然后取平均值
  2. 据我所知,线程。sleep不是精确的——它取决于操作系统如何切换你的线程
  3. 线程。加入不是立即的。同样,这取决于操作系统如何切换线程

更好的测试方法是在两个配置上运行计算密集型操作(例如,从1到100万求和)并计时。例如:

  1. 时间从1到100万的总和需要多长时间
  2. 一个线程从1求和到500000,另一个线程从500001求和到1000000需要多长时间

当你认为两个线程会比一个线程更快时,你是对的。但是并不是只有你的线程在运行——操作系统有线程,你的浏览器有线程,等等。请记住,你的时间不会是准确的,甚至可能会波动。

最后,还有其他原因(参见幻灯片24)导致线程工作速度较慢。