Interpolated strings: есть идеи, как подправить производител
От: Sinix  
Дата: 19.04.16 07:47
Оценка:
Про что речь:

в библиотеке CodeJam есть методы-ассерты с api вида
Code.AssertState(someCondition, "Message #123");
Code.AssertState(someCondition, "Message #{0}", 123);

ну, т.е. обычная перегрузка и вариант с строкой форматирования.

в 99.99% случаев ассерт не срабатывает, форматирование не требуется.


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

Как показывает практика, простое решение "не верьте советам решарпера" не работает
Будем искать сложное.

Матчасть:
Надо сказать, что мы тут не одиноки, на те же проблемы наткнулся, скажем, NLog.
Подписываюсь под

>We generally believe that libraries will mostly be written with different API names for methods which do different things. Therefore overload resolution differences between FormattableString and String don't matter, so string might as well win. Therefore we should stick with the simple principle that an interpolated string is a string. End of story.

Assumptions are the Base of evil.


Обсуждения этого дела в issues рослина:
https://github.com/dotnet/roslyn/issues/46
https://github.com/dotnet/roslyn/issues/10221


Предполагаемое решение (нам не подходит, т.к. к static-классу extension-метод не прикрутишь):
http://pvlerick.github.io/2016/01/poking-the-csharp-compiler-overload-resolution-for-string-and-formattablestring/

Бенчмарк:
         Formattable:   260ms, ips:          38 442 435,53 | Mem:  2 288,09 kb, GC 0/1/2: 279/0/0 => 10000000
    FormattableNoArg:   109ms, ips:          91 461 100,22 | Mem:  2 232,11 kb, GC 0/1/2: 101/0/0 => 10000000
      FormattableInt:    41ms, ips:         242 532 426,59 | Mem:    904,04 kb, GC 0/1/2: 76/0/0 => 10000000
              String:     3ms, ips:       3 317 850 033,18 | Mem:      8,00 kb, GC 0/1/2: 0/0/0 => 10000000
       String.Format:   125ms, ips:          79 656 647,98 | Mem:     64,04 kb, GC 0/1/2: 178/0/0 => 10000000
       FuncNoClosure:    28ms, ips:         344 932 255,31 | Mem:      8,00 kb, GC 0/1/2: 0/0/0 => 10000000
         FuncClosure:    91ms, ips:         109 542 823,03 | Mem:  1 376,06 kb, GC 0/1/2: 203/0/0 => 10000000
   FuncClosureFormat:    93ms, ips:         107 068 788,48 | Mem:  1 376,06 kb, GC 0/1/2: 203/0/0 => 10000000

Done.

  код
FormattableString как параметр не прикрутишь, таргетинг на 4.5 стоит, там этого типа нет.
        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)
        {
            Console.WindowWidth = 120;

            const int Count = 10 * 1000 * 1000;
            Measure("Formattable", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertState(true, $"Message #{i}");
                }
                return Count;
            });
            Measure("FormattableNoArg", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertState(true, $"Message #0");
                }
                return Count;
            });
            Measure("FormattableInt", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertState(true, i);
                }
                return Count;
            });
            Measure("String", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertStateMessage(true, "Message #0");
                }
                return Count;
            });
            Measure("String.Format", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertStateMessage(true, "Message #{0}", i);
                }
                return Count;
            });
            Measure("FuncNoClosure", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertStateFunc(true, () => "Message #0");
                }
                return Count;
            });
            Measure("FuncClosure", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertStateFunc(true, () => $"Message #{i}");
                }
                return Count;
            });
            Measure("FuncClosureFormat", () =>
            {
                for (int i = 0; i < Count; i++)
                {
                    AssertStateFunc(true, () => string.Format("Message #{0}", i));
                }
                return Count;
            });

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

        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 = 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);
        }


Собственно вопрос:
Ну и как с этим жить? В смысле, есть у кого-то опыт дизайна API, позволяющего и удовлетворить сторонников interpolated strings, и не убить при этом перфоманс приложения?

UPD Добавил пример с Func для ленивого получения строки.
Отредактировано 19.04.2016 8:41 Sinix . Предыдущая версия . Еще …
Отредактировано 19.04.2016 8:05 Sinix . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.