Доброго времени суток.
Краткое предисловие:
В процессе переноса кода с
FW 3.5 SP1 на
FW 4+, заметил на некоторых фрагментах существенную просадку производительности.
Проблема выявилась в неожиданном месте, а именно на строковых операциях наподобие
String.IndexOf, которые данный код активно использует.
Псевдо-код для демонстрации проблемы:
using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
StrBench(100000);
}
static void StrBench(int arrSize)
{
const int cnt = 10;
List<string> arr = new List<string>(arrSize);
for (int j = 0; j < arrSize; j++)
arr.Add(string.Format("{0}{1}{2}", new string('x', 100), BuildKey(j), new string('x', 100)));
string key = BuildKey(arrSize - 1);
int c = 0;
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < cnt; i++)
c += arr.Count(s => s.IndexOf(key) > -1);
double ts = sw.Elapsed.TotalSeconds;
ulong total = (ulong)arrSize * (ulong)cnt;
Console.WriteLine("String ({3}) {0} op / {1:0.000} sec = {2:0}", total, ts, total / ts, arrSize);
}
static string BuildKey(int i)
{
return i.ToString().PadLeft(10, '0');
}
}
Билдим:
Release (обязательно!)|Any CPU отдельно под FW 3.5 и FW 4
(я делал под 4.0 и 4.7.2)
Железка: Core i5-6500 (3.2-3.6 ГГц), 16 Gb (DDR4 1066Мгц) под Win10 x64
Результаты в среднем:
FW3.5 : String (100000) 1000000 op / 1,998 sec = 500451
FW4.0 : String (100000) 1000000 op / 5,480 sec = 182481
т.е. получаю регресс производительности в 2.7 раза (!)
Стоит отметить, что
на .Net Core 2.1 результат аналогичен FW 4+, что демонстрирует преемственность.
Компаратор используемый по умолчанию в IndexOf обсуждать не будем, понятно что можно использовать Ordinal, но суть не в этом.
Также стоит отметить, что проблема не в LINQ или чем-то еще, т.к. если заменить:
c += arr.Count(s => s.IndexOf(key) > -1);
на
c += arr.Count(s => s.Contains(key));
Получаем схожие результаты:
FW3.5 : String (100000) 1000000 op / 0,243 sec = 4110945
FW4.0 : String (100000) 1000000 op / 0,249 sec = 4016593
Собственно под FW 4+ код переносился с целью удобного распараллеливания при помощи LINQ, например:
c += arr.AsParallel().Count(s => s.IndexOf(key) > -1);
Под FW 4.0 результат (4 ядра):
FW4.0 : String (100000) 1000000 op / 1,505 sec = 664617
т.е. производительность в 3.5 раза больше на 4-х ядрах (под FW 4+), и при этом всего в 1.3 раза, чем на одном ядре под FW 3.5 SP1.
Вычислительная производительность одного ядра уже давно не сильно растет, если сравнить мою железку 5-ти летней давности (i5-6500) с железкой 10-ти летней давности (Core i3-540 на 3.06 Ггц) то получается, приблизительно в 1.5 раза в окрестностях почти равных частот.
http://www.cpu-world.com/Compare/365/Intel_Core_i3_i3-540_vs_Intel_Core_i5_i5-6500.html#bench1
На одном ядре профит (без учета увеличения частоты) обычно можно получить за счет более быстрого и емкого кэша и оптимизации конвейера.
В виду вышесказанного, всё это выглядит непредумышленной диверсией
со стороны разработчиков FW, т.к. регресс производительности, некоторых часто-используемых операций при переходе на FW 4+ существенно превышает многолетний прогресс по IPC на одно ядро.
С уменьшением техпроцесса по-прежнему, увеличивается энергоэффективность и кол-во ядер, за счет чего можно повысить производительность, но не все ведь можно распараллелить, и не всегда это легко.
В данном случае код местами переписан, проблемы нет (плюс профит от распараллеливания средствами LINQ), но беспокоит сам факт, что пришлось это делать.
P.S. Под .NET пишу с 1-й беты (2001 год), до этого C/C++ под x86, а еще ранее ASM под 3 МГЦ-овые 8-ми битки — i8080 (Z80) ...