Почему не делает моего потокового масштаба приложения .NET линейно при выделении больших объемов памяти?

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

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

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

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

Первый пример начинается с одним потоком, затем два потока и в конечном счете запускает тест с восемью потоками. Каждый поток создает 10 000 строк 1 024 символов каждый:

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 так, чтобы каждое ядро получило свою собственную "кучу" и поток сборщика "мусора".

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

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

Строковое интернирование

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

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

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

Исходный код

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 Range(this int max)
    {
      for (int counter = 0; counter < max; counter++)
      {
        yield return counter;
      }
    }

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

Приложение. Конфигурация



  
    
  

Выполнение примера

Для выполнения StackOverflowExample.exe на поле назовите его с этими параметрами командной строки:

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

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

7
задан user141682 15 January 2010 в 15:39
поделиться

5 ответов

Вы можете посмотреть, что этот вопрос мой .

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

5
ответ дан 7 December 2019 в 07:45
поделиться

Numpy имеет функцию под названием histogram2d , чье закрепление также показывает, как визуализировать его с помощью Matplotlib. Добавьте interpolation = ближайший к вызову imshow, чтобы отключить интерполяцию.

-121--2222149-

Можно попробовать ohloh поиск по языку .

-121--4167163-

Оборудование, на котором вы работаете, не способно к линейному масштабированию нескольких процессов или потоков.

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

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

На один 2 ЦП приходится пакет кэш-памяти l2. это бутылочное горлышко. Если кэш исчерпан, возникнут проблемы с последовательностью кэш-памяти.

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

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

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

2
ответ дан 7 December 2019 в 07:45
поделиться

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

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

0
ответ дан 7 December 2019 в 07:45
поделиться

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

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

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

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

0
ответ дан 7 December 2019 в 07:45
поделиться

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

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

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

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

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

.
0
ответ дан 7 December 2019 в 07:45
поделиться
Другие вопросы по тегам:

Похожие вопросы: