Почему мое многопоточное приложение .Net не масштабируется линейно при выделении больших объемов памяти? - PullRequest
7 голосов
/ 15 января 2010

Я столкнулся с чем-то странным в отношении влияния большого объема памяти на масштабируемость среды выполнения .Net. В моем тестовом приложении я создаю множество строк в узком цикле для фиксированного числа циклов и выплевываю скорость повторений цикла в секунду. Странность проявляется, когда я запускаю этот цикл в нескольких потоках - кажется, что скорость не увеличивается линейно. Проблема становится еще хуже, когда вы создаете большие строки.

Позвольте мне показать вам результаты. Моя машина - 8-гигабайтная 8-гигабайтная коробка под управлением Windows Server 2008 R1, 32-битная Он имеет два 4-ядерных процессора Intel Xeon 1,83 ГГц (E5320). Выполненная «работа» представляет собой набор чередующихся вызовов на ToUpper() и ToLower() в строке. Я запускаю тест для одного потока, двух потоков и т. Д. - по максимуму. Столбцы в таблице ниже:

  • Оценить: Количество циклов во всех потоках, деленное на продолжительность.
  • Линейная скорость: идеальная скорость, если производительность будет линейно масштабироваться. Он рассчитывается как скорость, достигнутая одним потоком, умноженная на количество потоков для этого теста.
  • Дисперсия: Рассчитывается как процент, на который курс не соответствует линейному курсу.

Пример 1: 10000 циклов, 8 потоков, 1024 символа на строку

Первый пример начинается с одного потока, затем с двух потоков и в итоге запускает тест с восемью потоками. Каждый поток создает 10000 строк по 1024 символа в каждой:

Creating 10000 strings per thread, 1024 chars each, using up to 8 threads
GCMode = Server

Rate          Linear Rate   % Variance    Threads
--------------------------------------------------------
322.58        322.58        0.00 %        1
689.66        645.16        -6.90 %       2
882.35        967.74        8.82 %        3
1081.08       1290.32       16.22 %       4
1388.89       1612.90       13.89 %       5
1666.67       1935.48       13.89 %       6
2000.00       2258.07       11.43 %       7
2051.28       2580.65       20.51 %       8
Done.

Пример 2: 10 000 циклов, 8 потоков, 32 000 символов на строку

Во втором примере я увеличил число символов для каждой строки до 32 000.

Creating 10000 strings per thread, 32000 chars each, using up to 8 threads
GCMode = Server

Rate          Linear Rate   % Variance    Threads
--------------------------------------------------------
14.10         14.10         0.00 %        1
24.36         28.21         13.64 %       2
33.15         42.31         21.66 %       3
40.98         56.42         27.36 %       4
48.08         70.52         31.83 %       5
61.35         84.63         27.51 %       6
72.61         98.73         26.45 %       7
67.85         112.84        39.86 %       8
Done.

Обратите внимание на разницу в отклонениях от линейной скорости; во второй таблице фактическая ставка на 39% меньше, чем линейная ставка.

Мой вопрос: Почему это приложение не масштабируется линейно?

Мои наблюдения

Ложный обмен

Сначала я думал, что это может быть связано с ложным совместным использованием, но, как вы увидите в исходном коде, я не делюсь ни одной коллекцией, и строки довольно большие. Единственное перекрытие, которое может существовать, - это начало одной строки и конец другой.

Сборщик мусора в режиме сервера

Я использую gcServer enabled = true, чтобы каждое ядро ​​получило свой собственный поток кучи и сборщика мусора.

Куча больших объектов

Я не думаю, что объекты, которые я выделяю, отправляются в кучу больших объектов, потому что они имеют размер менее 85000 байт.

String Interning

Я думал, что строковые значения могут быть разделены из-за интернирования MSDN , поэтому я попытался компилировать интернирование отключено. Это дало худшие результаты, чем показано выше

Другие типы данных

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

Исходный код

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Runtime;
using System.Runtime.CompilerServices;

namespace StackOverflowExample
{
  public class Program
  {
    private static int columnWidth = 14;

    static void Main(string[] args)
    {
      int loopCount, maxThreads, stringLength;
      loopCount = maxThreads = stringLength = 0;
      try
      {
        loopCount = args.Length != 0 ? Int32.Parse(args[0]) : 1000;
        maxThreads = args.Length != 0 ? Int32.Parse(args[1]) : 4;
        stringLength = args.Length != 0 ? Int32.Parse(args[2]) : 1024;
      }
      catch
      {
        Console.WriteLine("Usage: StackOverFlowExample.exe [loopCount] [maxThreads] [stringLength]");
        System.Environment.Exit(2);
      }

      float rate;
      float linearRate = 0;
      Stopwatch stopwatch;
      Console.WriteLine("Creating {0} strings per thread, {1} chars each, using up to {2} threads", loopCount, stringLength, maxThreads);
      Console.WriteLine("GCMode = {0}", GCSettings.IsServerGC ? "Server" : "Workstation");
      Console.WriteLine();
      PrintRow("Rate", "Linear Rate", "% Variance", "Threads"); ;
      PrintRow(4, "".PadRight(columnWidth, '-'));

      for (int runCount = 1; runCount <= maxThreads; runCount++)
      {
        // Create the workers
        Worker[] workers = new Worker[runCount];
        workers.Length.Range().ForEach(index => workers[index] = new Worker());

        // Start timing and kick off the threads
        stopwatch = Stopwatch.StartNew();
        workers.ForEach(w => new Thread(
          new ThreadStart(
            () => w.DoWork(loopCount, stringLength)
          )
        ).Start());

        // Wait until all threads are complete
        WaitHandle.WaitAll(
          workers.Select(p => p.Complete).ToArray());
        stopwatch.Stop();

        // Print the results
        rate = (float)loopCount * runCount / stopwatch.ElapsedMilliseconds;
        if (runCount == 1) { linearRate = rate; }

        PrintRow(String.Format("{0:#0.00}", rate),
          String.Format("{0:#0.00}", linearRate * runCount),
          String.Format("{0:#0.00} %", (1 - rate / (linearRate * runCount)) * 100),
          runCount.ToString()); 
      }
      Console.WriteLine("Done.");
    }

    private static void PrintRow(params string[] columns)
    {
      columns.ForEach(c => Console.Write(c.PadRight(columnWidth)));
      Console.WriteLine();
    }

    private static void PrintRow(int repeatCount, string column)
    {
      for (int counter = 0; counter < repeatCount; counter++)
      {
        Console.Write(column.PadRight(columnWidth));
      }
      Console.WriteLine();
    }
  }

  public class Worker
  {
    public ManualResetEvent Complete { get; private set; }

    public Worker()
    {
      Complete = new ManualResetEvent(false);
    }

    public void DoWork(int loopCount, int stringLength)
    {
      // Build the string
      string theString = "".PadRight(stringLength, 'a');
      for (int counter = 0; counter < loopCount; counter++)
      {
        if (counter % 2 == 0) { theString.ToUpper(); }
        else { theString.ToLower(); }
      }
      Complete.Set();
    }
  }

  public static class HandyExtensions
  {
    public static IEnumerable<int> Range(this int max)
    {
      for (int counter = 0; counter < max; counter++)
      {
        yield return counter;
      }
    }

    public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
    {
      foreach(T item in items)
      {
        action(item);
      }
    }
  }
}

App.Config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <gcServer enabled="true"/>
  </runtime>
</configuration>

Запуск примера

Чтобы запустить StackOverflowExample.exe на вашем компьютере, вызовите его с параметрами командной строки:

StackOverFlowExample.exe [loopCount] [maxThreads] [stringLength]

  • loopCount: сколько раз каждый поток будет манипулировать строкой.
  • maxThreads: количество потоков, к которым нужно перейти.
  • stringLength: количество символов для заполнения строки.

Ответы [ 5 ]

5 голосов
/ 15 января 2010

Возможно, вы захотите посмотреть, этот мой вопрос .

Я столкнулся с аналогичной проблемой, которая была связана с тем, что CLR выполняет межпотоковую синхронизацию при выделении памяти, чтобы избежать дублирования выделений. Теперь на сервере GC алгоритм блокировки может быть другим, но что-то в этом духе может повлиять на ваш код.

2 голосов
/ 15 января 2010

Аппаратное обеспечение, на котором вы работаете, не поддерживает линейное масштабирование нескольких процессов или потоков.

У вас есть один банк памяти. это узкое место (многоканальная память может улучшить доступ, но не для большего количества прецессии, чем у вас есть банки памяти (похоже, процессор e5320 поддерживает 1 - 4 канала памяти).

Существует только один контроллер памяти для каждого физического процессора (два в вашем случае), это бутылочное горлышко.

В каждом пакете процессоров содержится 2 кеша по 2 l2. это горлышко бутылки. Проблемы с когерентностью кэша произойдут, если этот кэш исчерпан.

это даже не касается проблем OS / RTL / VM при управлении расписанием процессов и управления памятью, что также способствует нелинейному масштабированию.

Я думаю, вы получаете довольно разумные результаты. Значительное ускорение с несколькими потоками и с каждым шагом до 8 ...

Правда, вы когда-нибудь читали что-либо, что бы указывало на то, что обычное аппаратное обеспечение с несколькими процессорами может линейно масштабировать несколько процессов / потоков? У меня нет.

0 голосов
/ 15 января 2010

Отличный вопрос, Люк! Я очень заинтересован в ответе.

Я подозреваю, что вы не ожидали линейного масштабирования, но что-то лучше, чем 39% дисперсии.

NoBugz - на основе ссылок 280Z28 фактически будет куча GC на ядро ​​с GCMode = Server. В каждой куче также должен быть поток GC. Это не должно привести к проблемам параллелизма, о которых вы упомянули?

столкнулся с аналогичной проблемой, которая была из-за того, что CLR выполняет синхронизация между потоками, когда выделение памяти, чтобы избежать наложения распределение. Теперь, с сервером GC, алгоритм блокировки может быть другим - но что-то в том же духе может повлиять на ваш код

Л.Бушкин - Я думаю, что это ключевой вопрос, вызывает ли GCMode = Server межпотоковую блокировку при выделении памяти? Кто-нибудь знает - или это можно объяснить аппаратными ограничениями, упомянутыми в SuperMagic?

0 голосов
/ 15 января 2010

Влияние распределителя памяти на приложение ускорение более тесно связано с количеством выделений , чем с выделенным количеством . На него также больше влияет задержка выделения (количество времени для завершения одного выделения в одном потоке), которая в случае CLR является чрезвычайно быстрой из-за использования распределителя-указателя (см. Раздел 3.4). 0,3) .

Ваш вопрос состоит в том, почему фактическое ускорение является сублинейным, и для ответа на него вы обязательно должны пересмотреть Закон Амдала .

Возвращаясь к Замечаниям по сборщику мусора CLR , вы можете видеть, что контекст выделения принадлежит определенному потоку (раздел 3.4.1), что уменьшает (но не устраняет) количество синхронизация требуется во время многопоточных распределений. Если вы обнаружите, что распределение действительно является слабым местом, я бы предложил попробовать пул объектов (возможно, для каждого потока), чтобы уменьшить нагрузку на сборщик. Уменьшая количество выделений, вы уменьшите количество раз, которое должен работать коллектор. Однако это также приведет к тому, что к поколению 2 попадет больше объектов, которые собирать медленнее всего, когда это необходимо.

Наконец, Microsoft продолжает улучшать сборщик мусора в новых версиях CLR, поэтому вам следует ориентироваться на самую последнюю версию, на которую вы способны (.NET 2 как минимум).

0 голосов
/ 15 января 2010

Ваш первоначальный пост в корне ошибочен - вы предполагаете, что линейное ускорение возможно благодаря параллельному выполнению. Это не так и никогда не было. См. Закон Амдала (Да, я знаю, Википедия, но это проще, чем что-либо еще).

Ваш код, рассматриваемый из абстракции, предоставляемой CLR, похоже, не имеет зависимостей - однако, как указал Л.Бушкин, это не так. Как указал SuperMagic, само оборудование подразумевает зависимости между потоками исполнения. Это справедливо практически для любой проблемы, которая может быть распараллелена - даже с независимыми машинами, с независимым оборудованием, некоторая часть проблемы обычно требует некоторого элемента синхронизации, и эта синхронизация предотвращает линейное ускорение.

...