Двухъядерная производительность хуже, чем одноядерная? - PullRequest
7 голосов
/ 26 декабря 2011

В следующем тесте nunit сравнивается производительность между работой одного потока и двух потоков на двухъядерной машине. В частности, это двухъядерный виртуальный компьютер с ОС Windows 7 под управлением VMWare, работающий на четырехъядерном хосте Linux SLED с Dell Inspiron 503.

Каждый поток просто зацикливает и увеличивает на 2 счетчика: addCounter и readCounter. Этот тест был оригинальным тестированием реализации Queue, которая, как было обнаружено, работала хуже на многоядерной машине. Таким образом, сужая проблему до небольшого воспроизводимого кода, вы получаете здесь без очереди только инкрементные переменные и, к шоку и тревоге, гораздо медленнее с 2 потоками, чем с одним.

При запуске первого теста диспетчер задач показывает, что одно из ядер на 100% занято, а другое ядро ​​почти бездействует. Вот результат теста для однопотокового теста:

readCounter 360687000
readCounter2 0
total readCounter 360687000
addCounter 360687000
addCounter2 0

Вы видите более 360 миллионов приращений!

Далее тест с двумя потоками показывает 100% занятость на обоих ядрах в течение всей 5-секундной продолжительности теста. Однако его вывод показывает только:

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

Это всего 223 миллиона приращений чтения. Что сотворило Бог, что эти два процессора делают в течение этих 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 "синхронизируется", поэтому только один поток за один раз может работать с этим методом. Однако мое тестирование говорит, что это не так. Так что до сих пор неясно, что происходит.

Но моя собственная проблема, похоже, решается простым созданием отдельных экземпляров методов для запуска потоков в качестве отправной точки.

С уважением, Wayne

EDIT:

Вот обновленный модульный тест, который тестирует потоки 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 ]

5 голосов
/ 26 декабря 2011

Это всего 223 миллиона приращений чтения. Что сотворило Бог, что эти два процессора делают в течение этих 5 секунд, чтобы выполнить меньше работы?

Возможно, вы столкнулись с конфликтом в кэше - когда один процессор увеличивает ваше целое число, он может сделать это в своем собственном кэше L1, но как только два процессора начинают "бороться" за одно значение, строка кэша он должен копироваться туда и обратно между их кешами каждый раз, когда каждый обращается к нему. Дополнительное время, затрачиваемое на копирование данных между кэшами, составляет fast , особенно когда выполняемая вами операция (увеличение целого числа) настолько тривиальна.

0 голосов
/ 26 декабря 2011

Несколько вещей:

  1. Вам, вероятно, следует протестировать каждую настройку как минимум 10 раз и взять среднее значение
  2. Насколько я знаю, Thread.sleep не является точным - это зависит от того, как ОС переключает ваши потоки
  3. Thread.join не является немедленным. Опять же, это зависит от того, как ОС переключает ваши потоки

Лучшим способом проверки было бы запустить вычислительно-интенсивную операцию (скажем, сумма от одного до миллиона) на двух конфигурациях и рассчитать их время. Например:

  1. Время, необходимое для суммирования от одного до миллиона
  2. Время, необходимое для суммирования одного до 500000 в одном потоке и одного 500001 до 1000000 в другом

Вы были правы, когда думали, что два потока будут работать быстрее, чем один поток. Но у вас не единственные запущенные потоки - в ОС есть потоки, в вашем браузере есть потоки и так далее. Имейте в виду, что ваши сроки не будут точными и могут даже колебаться.

Наконец, есть другие причины (см. Слайд 24), почему потоки работают медленнее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...