Производительность работы со строками. Что я делаю не так?
От: Artem Korneev США https://www.linkedin.com/in/artemkorneev/
Дата: 21.12.16 19:08
Оценка: 22 (1)
По мотивам вот этого
Автор: Sinix
Дата: 19.04.16
поста.

  Код.
static void AssertState(bool condition, string message)
{
    if (!condition)
    {
        throw new InvalidOperationException(message);
    }
}

static void AssertState(bool condition, IFormattable message)
{
    if (!condition)
    {
        throw new InvalidOperationException(message.ToString());
    }
}

static void AssertStateMessage(bool condition, string message)
{
    if (!condition)
    {
        throw new ArgumentException(message);
    }
}

static void AssertStateMessage(bool condition, string messageFormat, params object[] args)
{
    if (!condition)
    {
        throw new ArgumentException(string.Format(messageFormat, args));
    }
}

static void AssertStateFunc(bool condition, Func<string> messageCallback)
{
    if (!condition)
    {
        throw new InvalidOperationException(messageCallback());
    }
}

static void Main(string[] args)
{
    const int Count = 10 * 1000 * 1000;
    Measure("Concatenation", () =>
    {
        for (int i = 0; i < Count; i++)
        {
            AssertState(true, "Message '" + i + "': " + i + "." + i + "!");
        }
        return Count;
    });
    Measure("Interpolation", () =>
    {
        for (int i = 0; i < Count; i++)
        {
            AssertState(true, $"Message '{i}':{i}.{i}!");
        }
        return Count;
    });
    Measure("String", () =>
    {
        for (int i = 0; i < Count; i++)
        {
            AssertState(true, "Message #0");
        }
        return Count;
    });
    Measure("String.Format", () =>
    {
        for (int i = 0; i < Count; i++)
        {
            AssertStateMessage(true, "Message '{0}':{0}.{0}!", i);
        }
        return Count;
    });

    Console.WriteLine();
    Console.WriteLine("Done.");
}

static void Measure(string name, Func<long> callback)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    var mem = GC.GetTotalMemory(true);
    var gc00 = GC.CollectionCount(0);
    var gc01 = GC.CollectionCount(1);
    var gc02 = GC.CollectionCount(2);

    var sw = System.Diagnostics.Stopwatch.StartNew();
    var result = callback();
    sw.Stop();

    var mem2 = GC.GetTotalMemory(false);
    var gc10 = GC.CollectionCount(0);
    var gc11 = GC.CollectionCount(1);
    var gc12 = GC.CollectionCount(2);

    var memDelta = (mem2 - mem) / 1024.0;
    var gcDelta0 = gc10 - gc00;
    var gcDelta1 = gc11 - gc01;
    var gcDelta2 = gc12 - gc02;

    Console.WriteLine(
        "{0,20}: {1,5}ms, ips: {2,22:N} | Mem: {3,9:N2} kb, GC 0/1/2: {4}/{5}/{6} => {7,6}",
        name, sw.ElapsedMilliseconds, result / sw.Elapsed.TotalSeconds, memDelta, gcDelta0, gcDelta1, gcDelta2, result);
}


Всё почти как в первоисточнике, разница лишь в том, что я делаю конкатенацию/форматирование с несколькими элементами, не с одним. Но результаты у меня получаются совершенно другие:

       Concatenation:  7545ms, ips:           1,325,367.80 | Mem:    955.69 kb, GC 0/1/2: 1351/0/0 => 10000000
       Interpolation:  7907ms, ips:           1,264,567.77 | Mem:    265.71 kb, GC 0/1/2: 968/0/0 => 10000000
              String:    58ms, ips:         170,960,920.04 | Mem:      8.00 kb, GC 0/1/2: 0/0/0 => 10000000
       String.Format:   303ms, ips:          32,982,759.58 | Mem:  1,058.69 kb, GC 0/1/2: 133/0/0 => 10000000

Интерполяция, получается, медленнее даже простой конкатенации. Проверял, собирая под разные фреймворки, от 4.0 до 4.6.2, плюс dotnet core, результаты примерно одинаковые.
Что я делаю не так?
С уважением, Artem Korneev.
Отредактировано 22.12.2016 2:04 VladD2 . Предыдущая версия . Еще …
Отредактировано 21.12.2016 23:06 Artem Korneev . Предыдущая версия .
Отредактировано 21.12.2016 23:05 Artem Korneev . Предыдущая версия .
Re: Производительность работы со строками. Что я делаю не так?
От: Sinix  
Дата: 21.12.16 19:24
Оценка: 6 (1)
Здравствуйте, Artem Korneev, Вы писали:

AK>Интерполяция, получается, медленнее даже простой конкатенации. Проверял, собирая под разные фреймворке, от 4.0 до 4.6.2, плюс dotnet core, результаты примерно одинаковые.

AK>Что я делаю не так?

Да всё так, интерполяция ~= string.Format().

В исходном посте проблема в другом. Цитата:

В чём проблема: рекомендации решарпера заменяют второй вариант на
Code.AssertState(someCondition, $"Message #{123}");
, компилятор превращает эту строчку в
Code.AssertState(someCondition, string.Format("Message #{0}", 123));

т.е. форматирование строки происходит всегда.

Все остальные методы (кроме явного concat) делают форматирование лениво. Как пример:
        static void AssertStateMessage(bool condition, string messageFormat, params object[] args)
        {
            if (!condition)
            {
                throw new ArgumentException(string.Format(messageFormat, args));
            }
        }

Во всех вызовах condition == true, т.е. форматирование пропускается. остаются только накладные расходы на передачу параметров (в примере — массива args).
Re[2]: Производительность работы со строками. Что я делаю не
От: Artem Korneev США https://www.linkedin.com/in/artemkorneev/
Дата: 21.12.16 21:05
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Да всё так, интерполяция ~= string.Format().

S>В исходном посте проблема в другом.

Да, это я помню, но меня сейчас интересовало именно сравнение производительности разных вариантов работы со строками.

S>Во всех вызовах condition == true, т.е. форматирование пропускается.


Во! Вот тут я и ошибся. Потому и получалось, что форматирование у меня быстрее работало — оно ж просто пропускалось.
Но теперь у меня получается, что простая конкатенация быстрее, чем любое форматирование, хоть и жрёт чуть больше памяти, чем интерполяция (но меньше, чем форматирование):

       Concatenation:  6805ms, ips:           1,469,327.34 | Mem:    910.43 kb, GC 0/1/2: 1351/0/0 => 10000000
       Interpolation:  8016ms, ips:           1,247,403.86 | Mem:    273.66 kb, GC 0/1/2: 968/0/0 => 10000000
              String:    58ms, ips:         170,338,308.92 | Mem:      8.00 kb, GC 0/1/2: 0/0/0 => 10000000
       String.Format:  8068ms, ips:           1,239,374.06 | Mem:  2,003.69 kb, GC 0/1/2: 929/0/0 => 10000000

По крайней мере, для строки с семью элементами:

"Message '" + i + "': " + i + "." + i + "!"


Что как-то.. странно.
С уважением, Artem Korneev.
Отредактировано 22.12.2016 2:09 VladD2 . Предыдущая версия .
Re[3]: Производительность работы со строками. Что я делаю не
От: Sinix  
Дата: 21.12.16 21:18
Оценка: 22 (4)
Здравствуйте, Artem Korneev, Вы писали:

AK>Но теперь у меня получается, что простая конкатенация быстрее, чем любое форматирование

Оно так и должно быть. Конкатенация вызывает string.Concat, дальше под капотом или частный случай (до четырёх аргументов), или магия с StringBuilderCache.
См https://referencesource.microsoft.com/#mscorlib/system/string.cs,3017

StringFormat и ко заметно сложнее и предполагает как минимум разбор строки форматирования.

UPD: на самом деле c string.Concat всё немного сложнее, вчера лень было расписывать. Если коротко — есть две группы перегрузок string.Concat:
* с 2..4 аргументами типа string + перегрузка, которая принимает массив string
* с 2..3 аргументами типа object + перегрузка, которая принимает массив object

В теории любое сложение с пятью строками или больше приводит к аллокации массива,
любое сложение 4 (и больше) произвольных значений, одно из которых строка — то же самое + возможный boxing слагаемых-структур (custom operators не рассматриваем).

На практике всё немножко сложнее, например:
            string s = " ";
            int i = 0;
            char c = ' ';
            string r;
            r = "Hello," + " " + "world" + "!"; // a = "Hello, world!"
            r = "Hello," + s + "world" + "!";   // a = string.Concat<string>("Hello,", s, " world!")
            r = "Hello," + s + "world" + s;     // a = string.Concat<string>("Hello,", s, " world", s)
            r = "Hello," + c + "world" + "!";   // a = string.Concat<string>("Hello,", c.ToStirng(), " world!")
            r = "Hello," + c + "world" + c;     // a = string.Concat<string>("Hello,", c.ToStirng(), " world!", c.ToStirng())
            r = "Hello," + i + "world" + "!";   // a = string.Concat<object>("Hello,", i, " world!")
            r = "Hello," + i + "world" + i;     // a = string.Concat<object>(new object[] { "Hello,", i, " world", i })

            Console.WriteLine(r);

компилятор не стесняется объединять литералы-константы и использовать более эффективный вариант с строками как минимум для char.

Подробнее —
http://stackoverflow.com/questions/288794/does-c-sharp-optimize-the-concatenation-of-string-literals
https://ericlippert.com/2013/06/17/string-concatenation-behind-the-scenes-part-one/
Отредактировано 22.12.2016 7:27 Sinix . Предыдущая версия . Еще …
Отредактировано 22.12.2016 6:16 Sinix . Предыдущая версия .
Re[4]: Производительность работы со строками. Что я делаю не
От: Kolesiki  
Дата: 22.12.16 13:05
Оценка: +1
Здравствуйте, Sinix, Вы писали:

S> r = "Hello," + i + "world" + "!"; // a = string.Concat<object>("Hello,", i, " world!")


Вот здесь момент заинтересовал: с какого перепоя целая переменная (для которой, очевидно, должна быть своя эффективная конвертация в строку) сначала боксится, затем передаётся бог знает куда, там разбоксивается, узнаётся, что это целое, затем только конвертируется в строку и после этого конкатенатится! Получается слишком много бестолковой работы ради единственной цели — вызывать для любых типов Concat<object>(). Я считаю это безобразнейшей реализацией компилятора.

Конкатенация (раз уж была выведена на уровень языка) обязана проверять типы аргументов и макимально эффективно строить вызов:

var s = "Hello, " + i + "th warrior!"; // => Concat("Hello, ", i.ToString(), "th warrior!")


На мой взгляд, это самое логичное и эффективное решение.
Re[5]: Производительность работы со строками. Что я делаю не
От: Sinix  
Дата: 22.12.16 13:12
Оценка:
Здравствуйте, Kolesiki, Вы писали:

K>Получается слишком много бестолковой работы ради единственной цели — вызывать для любых типов Concat<object>(). Я считаю это безобразнейшей реализацией компилятора.

Тёмное наследие первого шарпа. Предложения поправить есть, но будет это не в ближайшем релизе, т.к. не особенно актуально.

Если интересно — могу накидать ссылок про конкретные варианты.


K>Конкатенация (раз уж была выведена на уровень языка) обязана проверять типы аргументов и макимально эффективно строить вызов:

K>На мой взгляд, это самое логичное и эффективное решение.
Угу, это один из них
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.