Интересности      Книги      Утилиты    

8 сентября 2011 г.

Кеширование длительных вычислений с PostSharp

Интересный пример нашел в поставке PostSharp.

Суть в чем. Они предлагают как одно из использований использовать PostSharp и АОП для кеширования долгоиграющих или сложных операций. Честно говоря я для себя придумал много use case АОП, но к такому еще не пришел.

Итак, пусть есть некий метод GetDifficultResult, и пусть вычисления в нем занимают, скажем, больше 1с, что для нас долго. Подход заключается в том, чтобы закешировать разные возвращаемые значения этой функцией в зависимости от входные параметров. Дело в том, что, если выполнение вычислений в методе не зависит от времени, а только от входных параметров, которые выбираются из некоего конечного набора, то мы можем закешировать связку входные “параметры – результат функции” и при этом нету никакой необходимости вызывать эту функцию снова.

Итак, пример (пример как я говорил идет в поставке с посташарпом, но такой подход можно применять и без него):

    internal class Program
{
public static void Main( string[] args )
{
Console.WriteLine( "1 ->" + GetDifficultResult( 1 ) );
Console.WriteLine( "2 ->" + GetDifficultResult( 2 ) );
Console.WriteLine( "1 ->" + GetDifficultResult( 1 ) );
Console.WriteLine( "2 ->" + GetDifficultResult( 2 ) );
}

[Cache]
private static int GetDifficultResult( int arg )
{
// If the following text is printed, the method was not cached.
Console.WriteLine( "Some difficult work!" );
Thread.Sleep( 1000 );
return arg;
}
}


Итак есть сложная долговычисляемая функция GetDifficultResult, которая на вход принимает целое число и результат которой зависит только от входных параметров. Нам необходимо ускорить работу программы. Как это сделать? Если входной параметр выбирается из некоторого конечного набора или предположим в нашем случае статистически проверено, что чаще всего передаются входные параметры 1 и 2, то результат функции мы можем закешировать.

Суть кеширования заключается в том, чтобы перед вызовом функции проверить, если в кеше имеется посчитанное значение для такого параметра и если его нет, то вызвать функцию и посчитать, а по завершению подсчетов, внести входной параметр и результат в словарь. Если же для такого(таких) входных параметров в словаре окажется посчитанное значение, то функция не вызывается, а мы сразу возвращаем результат из словаря.


Это можно сделать и без применение АОП и без PostSharp. Но, например, если такие вещи нужно проделывать в нескольких местах или классах, то почему бы не использовать АОП – ведь это сквозной функционал (cross-cutting concern). Тогда при использовании АОП подхода такие методы достаточно пометить атрибутом.


Итак, реализация с АОП и PostSharp. Реализуем свой атрибут, унаследованный от OnMethodBoundaryAspect. OnMethodBoundaryAspect – это абстрактный класс реализованный как атрибут. Чтобы вызывать наш функционал  кеширования до вызова метода и после его завершения достаточно переопределить методы OnEntry и OnSuccess, но для того, чтобы проверить был ли правильно использован наш атрибут, переопределим еще два метода CompileTimeInitialize и CompileTimeValidate – которые на этапе компиляции будут проверять и выдавать ошибку компиляции, если атрибут использован на конструкторах, методах, что не возвращают значения, или с out параметрами. Ведь, например, не зачем маркировать void-методы, если они не возвращают значение.


Итак, реализация:



    [Serializable]
public sealed class CacheAttribute : OnMethodBoundaryAspect
{
// Some formatting strings to compose the cache key.
private MethodFormatStrings formatStrings;

// A dictionary that serves as a trivial cache implementation.
private static readonly Dictionary<string object> cache = new Dictionary<string object>();


// Validate the attribute usage.
public override bool CompileTimeValidate( MethodBase method )
{
// Don't apply to constructors.
if ( method is ConstructorInfo )
{
Message.Write( SeverityType.Error, "CX0001", "Cannot cache constructors." );
return false;
}

MethodInfo methodInfo = (MethodInfo) method;

// Don't apply to void methods.
if ( methodInfo.ReturnType.Name == "Void" )
{
Message.Write( SeverityType.Error, "CX0002", "Cannot cache void methods." );
return false;
}

// Does not support out parameters.
ParameterInfo[] parameters = method.GetParameters();
for ( int i = 0; i < parameters.Length; i++ )
{
if ( parameters[i].IsOut )
{
Message.Write( SeverityType.Error, "CX0003", "Cannot cache methods with return values." );
return false;
}
}

return true;
}


// At compile time, initialize the format string that will be
// used to create the cache keys.
public override void CompileTimeInitialize( MethodBase method, AspectInfo aspectInfo )
{
this.formatStrings = Formatter.GetMethodFormatStrings( method );
}

// Executed at runtime, before the method.
public override void OnEntry( MethodExecutionArgs eventArgs )
{
// Compose the cache key.
string key = this.formatStrings.Format(
eventArgs.Instance, eventArgs.Method, eventArgs.Arguments.ToArray() );

// Test whether the cache contains the current method call.
lock ( cache )
{
object value;
if ( !cache.TryGetValue( key, out value ) )
{
// If not, we will continue the execution as normally.
// We store the key in a state variable to have it in the OnExit method.
eventArgs.MethodExecutionTag = key;
}
else
{
// If it is in cache, we set the cached value as the return value
// and we force the method to return immediately.
eventArgs.ReturnValue = value;
eventArgs.FlowBehavior = FlowBehavior.Return;
}
}
}

// Executed at runtime, after the method.
public override void OnSuccess( MethodExecutionArgs eventArgs )
{
// Retrieve the key that has been computed in OnEntry.
string key = (string) eventArgs.MethodExecutionTag;

// Put the return value in the cache.
lock (cache)
{
cache[key] = eventArgs.ReturnValue;
}
}
}


В коде используется еще класс Formatter код которого я не привожу, так как смысл его просто форматировать значения.

Этот пример можно загрузить вместе с PostSharp, который бесплатен для community использования. Загрузить это все можно тут.


Несомненно, use case кеширования долгоиграющих вычислений с помощью средств АОП весьма интересен и я возьму себе его не заметку. Но почему только долгоиграющих? Если функция обращается к сервису и на основе полученных параметров возвращает результат от сервера – ее тоже можно закешировать. Так что такой use case достаточно широкий.

2 комментария:

  1. Естественно, хорошо забытое старое, но ведь не суть в PostSharp, при желании такие вещи можно организовать с любым АОП фреймворком или вообще даже без него.

    ОтветитьУдалить
  2. Саттар Имамов7 ноября 2014 г. в 23:13

    Данный модход называется мемоизация

    ОтветитьУдалить