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

среда, 16 февраля 2011, Роман Калита

Интересный пример нашел в поставке 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 кеширования долгоиграющих вычислений с помощью средств АОП весьма интересен и я возьму себе его не заметку.




Ищите нас в интернетах!

Комментарии

Свежие вакансии