Я столкнулся с чем-то странным об эффекте выделений памяти большой емкости на масштабируемости времени выполнения .NET. В моем тестовом приложении я создаю много строк в жестком цикле для постоянного числа циклов и выкладываю уровень повторений цикла в секунду. Странность входит, когда я выполняю этот цикл в нескольких потоках – кажется, что уровень не увеличивается линейно. Проблема становится еще хуже при создании больших строк.
Позвольте мне показать Вам результаты. Моя машина составляет 8 ГБ, поле с 8 ядрами, выполняющее Windows Server 2008 R1, 32-разрядный. Это имеет два процессора Intel Xeon 1.83ghz (E5320) с 4 ядрами. Выполненная "работа" является рядом переменных вызовов к ToUpper()
и ToLower()
на строке. Я запускаю тест для одного потока, двух потоков, и т.д. – до максимума. Столбцы в приведенной ниже таблице:
Первый пример начинается с одним потоком, затем два потока и в конечном счете запускает тест с восемью потоками. Каждый поток создает 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.
Во втором примере я увеличил число символов для каждой строки к 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
: количество символов для заполнения строки.Вы можете посмотреть, что этот вопрос мой .
Я столкнулся с аналогичной проблемой, которая была связана с тем, что CLR выполняет синхронизацию между нитью при выделении памяти, чтобы избежать перекрывающихся ассигнований. Теперь, с сервером GC, алгоритм блокировки может быть разным - но что-то вдоль тех же строк может влиять на ваш код.
Numpy имеет функцию под названием histogram2d , чье закрепление также показывает, как визуализировать его с помощью Matplotlib. Добавьте interpolation = ближайший
к вызову imshow, чтобы отключить интерполяцию.
Можно попробовать ohloh поиск по языку .
-121--4167163-Оборудование, на котором вы работаете, не способно к линейному масштабированию нескольких процессов или потоков.
У вас есть один банк памяти. это бутылочное горлышко (многоканальная память может улучшить доступ, но не для большей прецессии, чем у вас есть банки памяти (кажется, процессор e5320 поддерживает 1-4 канала памяти).
На каждый физический пакет процессора (в вашем случае два) приходится только один контроллер памяти, это бутылочное горлышко.
На один 2 ЦП приходится пакет кэш-памяти l2. это бутылочное горлышко. Если кэш исчерпан, возникнут проблемы с последовательностью кэш-памяти.
Это даже не доходит до проблем OS/RTL/VM в управлении планированием процессов и управлением памятью, что также будет способствовать нелинейному масштабированию.
Я думаю, что вы получаете довольно разумные результаты. Значительное ускорение с несколькими потоками и с каждым шагом до 8...
Правда, вы когда-нибудь читали что-нибудь, чтобы предположить, что товарное многопроцессорное оборудование способно к линейному масштабированию нескольких процессов/потоков? Я этого не делал.
Ваш исходный пост принципиально ошибочно - вы предполагаете, что линейное ускорение возможно благодаря параллельному исполнению. Это не так, и никогда не было. См. Закон AMDAHL (да, я знаю, Википедия, но его проще, чем что-либо еще).
Ваш код, рассматриваемый из абстракции CLR, по-видимому, не имеет зависимостей - однако, как указывала Любункин, не тот случай. Как отметил SuperMagic, сам оборудование подразумевает зависимости между нитями выполнения. Это верно только о любой проблеме, которая может быть распараллелизована - даже с независимыми машинами, с независимым аппаратным обеспечением, некоторая часть проблемы обычно требует некоторого элемента синхронизации, и что синхронизация предотвращает линейное ускорение.
Влияние распределения памяти на применение Speedup более тесно связано с числом , чем объем , выделенный . Это также больше влияет на задержку распределения (количество времени для завершения одного выделения на одну поток), что в случае CLR чрезвычайно быстрая благодаря использованию аллекатора распределения удара (см. Раздел 3.4 .3) .
Ваш вопрос спрашивает, почему фактическое ускорение сублинируется, и ответить на то, что вы обязательно рассмотрите Закона Амдаля .
Возвращаясь к примечаниям на коллекторе мусора CLR , вы можете видеть, что контекст распределения принадлежит определенной нити (раздел 3.4.1), что уменьшает (но не устраняет) количество Синхронизация требуется во время многопоточных распределений. Если вы обнаружите, что выделение по-настоящему слабое точка, я бы предложил попробовать пул объекта (возможно, на один нить), чтобы уменьшить нагрузку на коллектор. Уменьшите чистое количество распределений, вы сократите количество раз, когда коллектор должен запустить. Тем не менее, это также приведет к большему количеству объектов, что делает его поколением 2, что является самым медленным для сбора, когда это необходимо.
Наконец, Microsoft продолжает улучшать сборщик мусора в более новых версиях CLR, поэтому вы должны нацелиться на самую последнюю версию, которую вы сможете (.NET 2 в голый минимум).
Отличный вопрос, Люк! Мне очень интересен ответ.
Я подозреваю, что вы ожидали не линейного масштабирования, а чего-то лучшего, чем 39% дисперсия.
NoBugz - исходя из ссылок 280Z28, на самом деле была бы куча GC на ядро с GCMode=Server. На каждую кучу также должен быть поток GC. Это не должно приводить к проблемам с параллельностью, о которых вы упоминали?
столкнулась с аналогичной проблемой, которая заключалась в следующем в связи с тем, что CLR выполняет межпоточная синхронизация, когда выделение памяти во избежание перекрытия распределения. Теперь, с сервером GC, алгоритм блокировки может отличаться - но что-то подобное может повлиять на ваш код
LBushkin - думаю, это ключевой вопрос, вызывает ли по-прежнему GCMode=Server межпотоковую блокировку при выделении памяти? Кто-нибудь знает - или это можно просто объяснить аппаратными ограничениями, о которых упоминал SuperMagic?
.