- PVSM.RU - https://www.pvsm.ru -
В этой статье Джон Скит будет описывать как простейшие конструкции языка замедляют вашу программу и как их можно ускорить.
Как и в любой работе, сваязанной с производительностью приложений, результат может варьироваться в зависимости от условий (в частности, например, 64-разрядный JIT
может работать несколько иначе), и в большинстве случаев это не должно вас волновать. Несмотря на это, относительно небольшое количество разработчиков пишут продакшен-код, состоящий из большого количества микрооптимизаций. Потому, пожалуйста, не принимайте этот пост как призыв к усложнению кода ради иррациональной оптимизации, которая якобы ускорит вашу программу. Используйте это только там, где это реально может понадобиться.
Пусть, нарпимер, у нас есть тип SteppedPattern
(автор обсуждает оптимизацию на примере своей библиотеки, Noda Time
, — прим. перев.), у которого есть generic
-тип TBucket
. Отмечу только, что важно, что перед тем, как я буду парсить value, я хочу создать новый объект класса TBucket
. Идея состоит в том что крупицы информации складываются в Bucket
, где они парсятся. И после окончания операции они складываюся в ParseResult
. Так что каждая операция разбора строки требует создания экземпляра TBucket
. Как мы можем создавать их в случае с Generic — типами?
Мы можем это сделать вызвав конструктор типа без параметров. Я не хочу задумываться, есть ли у передаваемых типов такой конструктор, потому я просто добавлю ограничение new()
и вызову new TBucket()
.
// Somewhat simplified...
internal sealed class SteppedPattern<TResult, TBucket> : IParsePattern<TResult>
where TBucket : new()
{
public ParseResult<TResult> Parse(string value)
{
TBucket bucket = new TBucket();
// Rest of parsing goes here
}
}
Великолепно! Совсем просто. Однако, к сожалению, я упустил из виду, что эта единственная строка кода будет занимать у нас 75% времени выполнения разбора строки. А это всего лишь создание пустого Bucket
— самого простого класса, который разбирает самую простую строчку! Когда я это понял, меня это потрясло.
Наше исправление будет очень простым. Нам всего лишь надо сказать нашему типу, как создавать экземпляр объекта. Сделаем это при помощи делегата:
// Somewhat simplified...
internal sealed class SteppedPattern<TResult, TBucket> : IParsePattern<TResult>
{
private readonly Func<TBucket> bucketProvider;
internal SteppedPattern(Func<TBucket> bucketProvider)
{
this.bucketProvider = bucketProvider;
}
public ParseResult<TResult> Parse(string value)
{
TBucket bucket = bucketProvider();
// Rest of parsing goes here
}
}
Теперь я могу вызвать new StoppedPattern(() => new OffsetBucket())
, или что-то в этом духе. Это также означает, что я могу оставить коструктор как internal и больше никогда о нем не заботиться. И, что еще более упростит написание последующего кода, я даже смог бы использовать старые Bucket для разбора последующих строк.
Мне так кажется, что далеко не всем захочется прогонять тесты самостоятельно, а больше захочется посмотреть на готовые результаты. Потому я решил привести результаты benchmarks, которые я сделал чтобы проверить только время создания Generic-типов. Для того чтобы показать насколько незначительными будут эти результаты, я указжу, что значения, записанные в таблице, измеряются в миллисекундах. И за это время было выполнено 100 миллионов операций, которые мы будем тестировать. Потому если только ваш код не основан на частом обращении к операции создания generic-типов, это не должно вызвать в вас желание переписывать код. Однако, запомните это на будущее.
Так или иначе, наш код разработан для работы с четыремя типами: двумя классами и двумя структурами. И для каждого из них — с маленькой и большой версией (имеется в виду, видимо, маленькие и большие версии для GAC
, меньшие и большие чем 85K), на 32-х и 64-разрядных машинах, для CLR v2
, v4. 64-разрядная машина у меня сама по себе более быстрая, так что необходимо сравнивать результаты внутри одной машины.
CLR v4: 32-bit results (ms per 100 million iterations)
Test type | new() constraint | Provider delegate |
Small struct | 689 | 1225 |
Large struct | 11188 | 7273 |
Small class | 16307 | 1690 |
Large class | 17471 | 3017 |
CLR v4: 64-bit results (ms per 100 million iterations)
Test type | new() constraint | Provider delegate |
Small struct | 473 | 868 |
Large struct | 2670 | 2396 |
Small class | 8366 | 1189 |
Large class | 8805 | 1529 |
CLR v2: 32-bit results (ms per 100 million iterations)
Test type | new() constraint | Provider delegate |
Small struct | 703 | 1246 |
Large struct | 11411 | 7392 |
Small class | 143967 | 1791 |
Large class | 143107 | 2581 |
CLR v2: 64-bit results (ms per 100 million iterations)
Test type | new() constraint | Provider delegate |
Small struct | 510 | 686 |
Large struct | 2334 | 1731 |
Small class | 81801 | 1539 |
Large class | 83293 | 1896 |
Посмотрите на результаты для классов. Это реальные результаты — они занимают около 2-х минут на моем ноутбуке при использовании ограничения new()
и всего пару секунд при использовании провайдера. И, что очень важно отметить, эти результаты актуальны для .Net 2.0
(имеется в виду CLR
, а версия 2.0 скорее написана для того чтобы удивить читателя тем что вплоть до .Net 3.5
все работает на CLR v2
, для .Net 2.0
).
И, конечно же, вы можете скачать benchmark [1]чтобы посмотреть и убедиться как это отработает на вашей машине.
Насколько я понимаю, не существует никакой IL
инструкции чтобы поддержать ограничение new()
. Вместо этого компилятор вставляет инструкции вызова Activator.CreateInstance[T] [2]. Очевидно, это в любом случае медленнее вызова делегата, т.к. В этом случае мы пытаемся найти подходящий для нас конструктор через рефлексию и вызываем его. Меня по-настоящему удивило точто это не было соптимизировано. Ведь очевидное решение — это использование делегатов и их кэширование для дальшнейшего использования. Я не буду разводить дебатов по вопросам принятого ими решения, ведь в конечном итоге их решение не расходует дополнительной памяти, которую будет занимать кэш.
(взято из второй части статьи)
Здесь мы посмотрим на производительность работы с делегатами. А также попробуем их ускорить.
Полный исходный код тестирования производительности вы можете скачать у меня с сайта [3]. Здесь, по сути, я делаю аналогичные действия каждый раз, когда пишу тест. Создаю делегат типа Action
, который ничего не делает и проверяю что ссылка на него не является обнуленной. Это я делаю только чтобы избежать оптимизаций JIT
. Каждый тест выполнен в виде generic-метода, который принимает один Generic
-параметр. Я вызываю каждый метод два раза: в первый раз я передаю в качестве аргумента Int32
, а во второй — String
. Также а включил несколько кейсов:
private static void Lambda<T>()
{
Action foo = () => {};
if (foo == null)
{
throw new Exception();
}
}
private static void FakeCachedLambda<T>()
{
if (FakeLambdaCache<T>.CachedAction == null)
{
FakeLambdaCache<T>.CachedAction = FakeLambdaCache<T>.NoOp;
}
Action foo = FakeLambdaCache<T>.CachedAction;
if (foo == null)
{
throw new Exception();
}
}
private static class FakeLambdaCache<T>
{
internal static Action CachedAction;
internal static void NoOp() {}
}
generic
-метод, и будем делать method group conversion
private static void GenericMethodGroup<T>()
{
Action foo = NoOp<T>;
if (foo == null)
{
throw new Exception();
}
}
generic
метод, чтобы впоследствии применить method group conversion
private static void NonGenericMethodGroup<T>()
{
Action foo = NoOp;
if (foo == null)
{
throw new Exception();
}
}
method group conversion
в статическом не-generic
методе generic-типа;
private static void StaticMethodOnGenericType<T>()
{
Action foo = SampleGenericClass<T>.NoOpStatic;
if (foo == null)
{
throw new Exception();
}
}
method group conversion
в не статическом не-generic методе generic
-типа, с использованием generic
-классом кэша с единственным полем, указывающем на экземпляр generic
-класса. private static void InstanceMethodOnGenericType<T>()
{
Action foo = ClassHolder<T>.SampleInstance.NoOpInstance;
if (foo == null)
{
throw new Exception();
}
}
Также раскрою все нераскытые определения:
private static void NoOp() {}
private static void NoOp<T>() {}
private class ClassHolder<T>
{
internal static SampleGenericClass<T> SampleInstance = new SampleGenericClass<T>();
}
private class SampleGenericClass<T>
{
internal static void NoOpStatic()
{
}
internal void NoOpInstance()
{
}
}
Заметьте, что все это мы делаем в generic-методе, и вызываем его для каждого типа: Int32
и String
. И, что важно заметить, мы не захватываем никаких переменных, и generic-параметр не участвует ни в какой части реализации тела метода.
Опять же, результаты представлены в миллисекундах, на 10 миллионах операциях. Я не хачу запускать их на 100 миллиона операциях, потому что это будет очень медленно. Также уточню, что тестирование производилось на x64 JIT
Test | TestCase[int] | TestCase[string] |
Lambda expression | 180 | 29684 |
Generic cache class | 90 | 288 |
Generic method group conversion | 184 | 30017 |
Non-generic method group conversion | 178 | 189 |
Static method on generic type | 180 | 29276 |
Instance method on generic type | 202 | 299 |
Автор: SunexDevelopment
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/7912
Ссылки в тексте:
[1] benchmark : http://pobox.com/~skeet/csharp/blogfiles/NewConstraint.cs
[2] Activator.CreateInstance[T]: http://msdn.microsoft.com/en-us/library/0hcyx2kd.aspx
[3] у меня с сайта: http://pobox.com/~skeet/csharp/blogfiles/GenericsAndLambdas.cs
Нажмите здесь для печати.