Декораторы.
Март 30, 2009 – 10:37 ппПостоянное усложнение требований к программным системам ведет к неизбежному усложнению этих систем и на разработку приходится тратить все больше времени. Для решения этих проблем выход, как правило, один – повышение уровня абстракции кода и максимальное уменьшение дублирования кода.
Выполнения этих условий добиваются различными способами, например, использование различных паттернов проектирования. Однако существует направление, которое весьма широко применяется в различных вариантах – метапрограммирование. Метапрограммирование, на даный момент, развивается в двух направлениях: генерация кода и самомодифицирующийся код. Оба направления применяются уже очень давно и каждый из них обладает рядом преимуществ и недостатков.
Недавно была поставлена цель сымитировать на платформе .Net (C#) декораторы в том виде, в котором они существуют в Python. В связи с отсутствием какой либо языковой поддержки метапрограммирования в C# реализация такой конструкции, естественно, должна представлять собой решение основанное на подходах АОП. Перед такой реализацией были поставлены следующие условия:
- Реализация в виде аттрибута, чтобы использование декоратора было максимально приближено к python-реализации.
- Простота реализации – в идеале для реализации аттрибута хотелось максимум наследования и переопределения одного метода, а минимум – передачи делегата в конструктор аттрибута.
- Реализация аттрибута на основном языке проекта, т.е. на C#.
В результате недолгих поисков через Википедию была найдена библиотека PostSharp, цели которой совпадают с поставленными (повезло-то как :)).
Дальнейшей задачей стояло выяснение производительности решений с использованием разного рода платформ. Были протестированы платформы .Net (c# 3.5, IronPython 2.0), CPython 2.5. Для каждого варианта проводилось тестирование в виде обыкновенного и декорированного вызова метода. Всего делалось 1000000000 вызовов на каждый вариант. Python-реализация тестировалась без изменения кода на IronPython и CPython.
Результаты тестирования:
| Без декоратора | С декоратором | Понижение скорости | |
| Паттерн (C#) | 7 сек. | 14 сек. | 2 раза |
| Декоратор PostSharp | 7 сек. | 305 сек. | 43.5 раза |
| CPython 2.5 | 268 сек. | 653 сек. | 2.4 раза |
| IronPython 2.0 | 186 сек. | 431 сек. | 2.3 раза |
Тестовый код на C#:
using System;
internal class Program
{
private static void Main(string[] args)
{
foreach (var custom in new List<ICustom>
{
new NormalCustom(),
new DecoratorCustom(new NormalCustom()),
new PostSharpDecoratorCustom()
}
)
{
const int maxCycle = 1000000000;
long start = DateTime.Now.Ticks;
for (int i = 0; i lt; maxCycle; i++)
{
custom.Method(i, maxCycle);
}
Console.WriteLine(new DateTime(DateTime.Now.Ticks - start));
}
Console.ReadKey();
}
}
public interface ICustom
{
long Method(int r, int y);
}
// обычная реализация интерфейса ICustom
public class NormalCustom : ICustom
{
public long Method(int r, int y)
{
return r + y;
}
}
// реализация обычного паттерна (образца) Декоратор.
public class DecoratorCustom : ICustom
{
private readonly ICustom obj;
public DecoratorCustom(ICustom obj)
{
this.obj = obj;
}
public long Method(int r, int y)
{
return obj.Method(r + y, y);
}
}
// реализация паттерна декоратор на основе библиотеки PostSharp
using PostSharp.Laos;
[Serializable]
public sealed class CustomAttribute : OnMethodInvocationAspect
{
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
object[] args = eventArgs.GetArgumentArray();
eventArgs.ReturnValue = eventArgs.Delegate.DynamicInvoke(new[]
{
(int) args[0] + (int) args[1],
args[1]
});
}
}
// пример применения декоратора PostSharp
public class PostSharpDecoratorCustom : ICustom
{
[Custom]
public long Method(int r, int y)
{
return r + y;
}
}
Тестовый код Python:
import time
def decorator ( f ) :
def wrapper ( self, x, y ) :
return f( self, x + y, y )
return wrapper
class Decorated ( object ) :
@decorator
def method ( self, x, y ) :
return x + y
class Normal ( object ) :
def method ( self, x, y ) :
return x + y
def main() :
custom = Normal()
decorated_custom = Decorated()
max_cycle = 1000000000
start = time.time()
for i in xrange( max_cycle ) :
custom.method( i, max_cycle )
print( time.time() - start )
start = time.time()
for i in xrange( max_cycle ) :
decorated_custom.method( i, max_cycle )
print( time.time() - start )
if __name__ == '__main__' :
main()
Таким образом, реализация декораторов PostSharp оказалась весьма не шустрой, что в общем-то и ожидалось, т.к. реализация основывается на интроспекции кода. Полученные результаты нельзя сравнивать по абсолютным цифрам, т.к. это совершенно разные платформы, однако по относительному замедлению исполнения кода можно судить об эффективности реализации. Как видно из таблицы, обе реализации Python оказались весьма качественными и не намного уступают C#, хотя конечно если смотреть на абсолютные цифры, то декораторы PostSharp не так уж и сильно проигрывают. :)
Комментарии:
Интересная статья. Спасибо.
А ты не пробовал посмотреть что же именно тормозит в PostSharp сценарии? Что-то мне кажется, что основные тормоза вызваны рефлексией, а не самим постшарпом.
С другой стороны, работа с рефлексией навязана самим постшарпом, так что это так же его косяк.
By Aikin on Июнь 17, 2009
Естественно, тормоза из-за рефлексии. На самом деле показанные цифры – ещё не самый плохой результат, в .net 1.1 говорят ещё хуже было, но я его не застал. :)
Проблема данного решения именно в скорости – у меня постоянно идут проекты, связанные с высокими нагрузками, потому использовать декораторы не получается. Приходится принимать правила конкретной платформы. Есть надежда, что в последующих версиях Шарпа (5.0 и далее) введут какие-то элементы декларативности.
By Roinet on Июнь 17, 2009
Немного погонял у себя тесты. Вот результаты.
Первое:
Сравнивать чистую версию с постшарпом некорректно. Корректней было бы сравнивать все с “паттерным декоратором”.
Второе:
eventArgs.Delegate.DynamicInvoke — совсем не рефлексия, а вызов делегата.
Оказалось, что рефлексия показывает совсем запредельные тормоза (в 700 раз медленнее чем читый метод) о_0
Так что надеятся на изменения в рантайме не стоит.
Третье:
Во время вызова метода, процессор проводит в потрохах постшарпа примерно столько же времени сколько в вызове делегата.
Выводы делать как-то ломает.
P.S. Cтранно, что MethodInvocationEventArgs.Delegate помечен устаревшим и предлагается использовать MethodInvocationEventArgs.Method. Производительность рефлексии просто ужасает
By Aikin on Июнь 17, 2009
АОП в Java и .Net работает через рефлексию, и, естественно, оно медленное. Я собственно об этом и говорил. Только изменение принципов работы платформы поможет хоть как-то исправить ситуацию, а так удел библиотек типа PostSharp – использование в очень специфических случаях, где скорость не важна, а от декларативного представления задачи объем кода, времени, геммороя резко снижается.
На других платформах, типа python, использование декораторов более чем оправдано.
By Roinet on Июнь 17, 2009
ПостШарп не работает через рефлексию, он инструментирует сборку после билда.
Вот такую шнягу можно увидеть рефлектором:
[DebuggerNonUserCode, CompilerGenerated]
public long Method(int r, int y)
{
Delegate delegateInstance = new ~PostSharp~Laos~Implementation.~delegate~0(this.~Method);
object[] arguments = new object[] { r, y };
MethodInvocationEventArgs eventArgs = new MethodInvocationEventArgs(delegateInstance, arguments);
~PostSharp~Laos~Implementation.CustomAttribute~1.OnInvocation(eventArgs);
return (long) eventArgs.ReturnValue;
}
Если бы там была рефлексия ситуация была бы еще хуже (~700 раз).
По поводу динамических языков (питон, айронПитон): Ты не задумывался почему скрость работы чистого кода так низка? Языки с динамической типизацией привносят свои проблемы.
By Aikin on Июнь 17, 2009
Я прекрасно знаю про такие вещи как интерпретация и JIT-компиляция, а так же о преимуществах и недостатках динамической и статической типизации. :)
Статья была не про это. Речь о том, что PostSharp – хороший концепт, который однако бездумно использовать нельзя – медленно работает.
Если хочется пообщатся по поводу проблем динамической типизации, пишите на почту (адрес я оставлял у вас в блоге).
By Roinet on Июнь 17, 2009