Регресс производительности при переходе с FW 3.5 SP1 на FW 4+ (String.IndexOf)
От: 4058  
Дата: 16.05.20 11:31
Оценка: 1 (1)
Доброго времени суток.

Краткое предисловие:

В процессе переноса кода с 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) ...
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.