Я столкнулся с чем-то странным в отношении влияния большого объема памяти на масштабируемость среды выполнения .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
: количество символов для заполнения строки.