C# Memoization des fonctions avec un nombre arbitraire d'arguments

j'essaie de créer une interface de mémoization pour les fonctions avec un nombre arbitraire d'arguments, mais j'échoue lamentablement j'ai l'impression que ma solution n'est pas très flexible. J'ai essayé de définir une interface pour une fonction qui obtient memoized automatiquement lors de l'exécution et chaque fonction devra mettre en œuvre cette interface. Voici un exemple avec une fonction de moyenne mobile exponentielle à deux paramètres:

class EMAFunction:IFunction
{
    Dictionary<List<object>, List<object>> map;

    class EMAComparer : IEqualityComparer<List<object>>
    {
        private int _multiplier = 97;

        public bool Equals(List<object> a, List<object> b)
        {
            List<object> aVals = (List<object>)a[0];
            int aPeriod = (int)a[1];

            List<object> bVals = (List<object>)b[0];
            int bPeriod = (int)b[1];

            return (aVals.Count == bVals.Count) && (aPeriod == bPeriod);
        }

        public int GetHashCode(List<object> obj)
        {
            // Don't compute hash code on null object.
            if (obj == null)
            {
                return 0;
            }

            List<object> vals = (List<object>) obj[0];
            int period = (int) obj[1];

            return (_multiplier * period.GetHashCode()) + vals.Count;

        }
    }

    public EMAFunction()
    {
        NumParams = 2;
        Name = "EMA";
        map = new Dictionary<List<object>, List<object>>(new EMAComparer());
    }
    #region IFunction Members

    public int NumParams
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public object Execute(List<object> parameters)
    {
        if (parameters.Count != NumParams)
            throw new ArgumentException("The num params doesn't match!");

        if (!map.ContainsKey(parameters))
        {
            //map.Add(parameters,
            List<double> values = new List<double>();
            List<object> asObj = (List<object>)parameters[0];
            foreach (object val in asObj)
            {
                values.Add((double)val);
            }
            int period = (int)parameters[1];

            asObj.Clear();
            List<double> ema = TechFunctions.ExponentialMovingAverage(values, period);
            foreach (double val in ema)
            {
                asObj.Add(val);
            }
            map.Add(parameters, asObj);
        }
        return map[parameters];
    }

    public void ClearMap()
    {
        map.Clear();
    }

    #endregion
}

Voici mes tests de la fonction:

private void MemoizeTest()
{
    DataSet dataSet = DataLoader.LoadData(DataLoader.DataSource.FROM_WEB, 1024);
    List<String> labels = dataSet.DataLabels;

    Stopwatch sw = new Stopwatch();
    IFunction emaFunc = new EMAFunction();
    List<object> parameters = new List<object>();
    int numRuns = 1000;
    long sumTicks = 0;
    parameters.Add(dataSet.GetValues("open"));
    parameters.Add(12);

    // First call

    for(int i = 0; i < numRuns; ++i)
    {
        emaFunc.ClearMap();// remove any memoization mappings
        sw.Start();
        emaFunc.Execute(parameters);
        sw.Stop();
        sumTicks += sw.ElapsedTicks;
        sw.Reset();
    }
    Console.WriteLine("Average ticks not-memoized " + (sumTicks/numRuns));


    sumTicks = 0;
    // Repeat call
    for (int i = 0; i < numRuns; ++i)
    {
        sw.Start();
        emaFunc.Execute(parameters);
        sw.Stop();
        sumTicks += sw.ElapsedTicks;
        sw.Reset();
    }
    Console.WriteLine("Average ticks memoized " + (sumTicks/numRuns));
}

mise à Jour:

Merci d'avoir souligné mon erreur n00bish... J' toujours oubliez pas d'appeler la Réinitialisation du chronomètre!

j'ai vu une autre approche de memoization ... il n'offre pas n-memoization d'argument, mais mon approche avec L'Interface n'est pas beaucoup plus avantageux puisque je dois écrire une classe pour chaque fonction. Est-il raisonnable que je peux fusionner ces idées en quelque chose de plus robuste? je veux rendre plus facile de memoize une fonction sans faire écrire à l'utilisateur une classe pour chaque fonction qu'ils ont l'intention d'utiliser.

34
demandé sur Community 2010-05-17 23:35:29

5 réponses

et ça? Tout d'abord écrire un memoizer d'un argument:

static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var d = new Dictionary<A, R>();
    return a=> 
    {
        R r;
        if (!d.TryGetValue(a, out r))
        {
            r = f(a);
            d.Add(a, r);
        }
        return r;
    };
}  

simple. Maintenant, écrivez une fonction tulifier:

static Func<Tuple<A, B>, R> Tuplify<A, B, R>(this Func<A, B, R> f)
{
    return t => f(t.Item1, t.Item2);
}

et un det ulifier:

static Func<A, B, R> Detuplify<A, B, R>(this Func<Tuple<A, B>, R> f)
{
    return (a, b) => f(Tuple.Create(a, b));
}

et maintenant un memoizer à deux arguments est facile:

static Func<A, B, R> Memoize<A, B, R>(this Func<A, B, R> f)
{
    return f.Tuplify().Memoize().Detuplify();
}

pour écrire un memoizer à trois arguments il suffit de garder ce modèle: faire un 3-tulifier, un 3-untulifier, et un 3-memoizer.

de bien sûr, si vous n'en avez pas besoin, il n'y a pas besoin de rendre les méthodes nominales des tulifiers:

static Func<A, B, R> Memoize<A, B, R>(this Func<A, B, R> f)
{
    Func<Tuple<A, B>, R> tuplified = t => f(t.Item1, t.Item2);
    Func<Tuple<A, B>, R> memoized = tuplified.Memoize();
    return (a, b) => memoized(Tuple.Create(a, b));
}

mise à jour: vous demandez quoi faire s'il n'y a pas de type tuple. Tu pourrais écrire le tien, ce n'est pas difficile. Ou vous pouvez utiliser les types anonymes:

static Func<T, R> CastByExample<T, R>(Func<T, R> f, T t) { return f; }

static Func<A, B, R> Memoize<A, B, R>(this Func<A, B, R> f)
{
    var example = new { A=default(A), B=default(B) };
    var tuplified = CastByExample(t => f(t.A, t.B), example);
    var memoized = tuplified.Memoize();
    return (a, b) => memoized(new {A=a, B=b});
}

Slick, hein?


mise à jour: C# 7 a maintenant des tuples valeur intégrée à la langue; utilisez-les plutôt que de rouler votre propre ou en utilisant des types anonymes.

85
répondu Eric Lippert 2017-05-18 15:24:48

D'abord, vous devez appeler sw.Reset() entre vos tests. Dans le cas contraire, vos résultats pour le second test seront en plus de au temps à partir du premier.

Deuxièmement, vous ne devriez probablement pas utiliser vals.GetHashCode() dans votre GetHashCode() override sur le compareur, car cela vous mènera à obtenir des codes de hachage différents pour les objets qui évalueraient à true pour votre Equals override. Pour l'instant, je m'inquiéterais de s'assurer que objets équivalents toujours obtenir le même code de hachage plutôt que d'essayer d'obtenir une distribution égale des codes. Si les codes de hachage ne correspondent pas, Equals ne sera jamais appelé, donc vous finirez par traiter les mêmes paramètres plusieurs fois.

3
répondu Adam Robinson 2010-05-17 19:42:14

Chronomètre.Stop ne réinitialise pas le chronomètre donc vous accumulez du temps à chaque démarrage/arrêt.

par exemple

  Stopwatch sw = new Stopwatch();

  sw.Start();
  System.Threading.Thread.Sleep(100);
  sw.Stop();
  Debug.WriteLine(sw.ElapsedTicks);

  sw.Start();
  System.Threading.Thread.Sleep(100);
  sw.Stop();
  Debug.WriteLine(sw.ElapsedTicks);

donne les résultats suivants""

228221
454626

Vous pouvez utiliser StopWatch.Restart (Framework 4.0) pour redémarrer le chronomètre à chaque fois, ou si pas de Framework 4.0, vous pouvez utiliser StopWatch.Reset pour réinitialiser le chronomètre.

3
répondu Chris Taylor 2010-05-17 20:28:44

Alternative (pour les tuples et les types anonymes) approche pourrait être comme suit:

static void Main(string[] args)
{
    var func = Memoize<int, int, int>(Func);

    Console.WriteLine(func(3)(4));
    Console.WriteLine(func(3)(5));
    Console.WriteLine(func(2)(5));
    Console.WriteLine(func(3)(4));
}

//lets pretend this is very-expensive-to-compute function
private static int Func(int i, int j)
{
    return i + j;
}

private static Func<TArg1, Func<TArg2, TRes>> Memoize<TArg1, TArg2, TRes>(Func<TArg1, TArg2, TRes> func)
{
     Func<TArg1, Func<TArg2, TRes>> func1 = 
        Memoize((TArg1 arg1) => Memoize((TArg2 arg2) => func(arg1, arg2)));

    return func1;
}

private static Func<TArg, TRes> Memoize<TArg, TRes>(Func<TArg, TRes> func)
{
    var cache = new Dictionary<TArg, TRes>();

    return arg =>
    {
        TRes res;

        if( !cache.TryGetValue(arg, out res) )
        {
            Console.WriteLine("Calculating " + arg.ToString());

            res = func(arg);

            cache.Add(arg, res);
        }
        else
        {
            Console.WriteLine("Getting from cache " + arg.ToString());
        }

        return res;
    };
}

basé sur ces deux fonctions Memoize vous pouvez facilement construire des extensions pour autant d'args que vous le souhaitez.

2
répondu ay.metallo 2012-03-15 07:53:08

je suis d'abord venu ici à la recherche d'une méthode de mémoization abstraite pour une fonction sans paramètre. Ce n'est pas vraiment une réponse à la question mais je voulais partager ma solution au cas où quelqu'un d'autre viendrait chercher le cas simple.

public static class MemoizationExtensions
{
    public static Func<R> Memoize<R>(this Func<R> f)
    {
        bool hasBeenCalled = false; // Used to determine if we called the function and the result was the same as default(R)
        R returnVal = default(R);
        return () =>
        {
            // Should be faster than doing null checks and if we got a null the first time, 
            // we really want to memoize that result and not inadvertently call the function again.
            if (!hasBeenCalled)
            {
                hasBeenCalled = true;
                returnVal = f();
            }
            return returnVal;
        };
    }
}

si vous utilisez LinqPad vous pouvez utiliser le code suivant pour tester facilement la fonctionnalité en utilisant la méthode super cool Dump de LinqPad.

new List<Func<object>>(new Func<object>[] {
        () => { "Entered func A1".Dump(); return 1; },
        () => { "Entered func A2".Dump(); return default(int); },
        () => { "Entered func B1".Dump(); return String.Empty; },
        () => { "Entered func B2".Dump(); return default(string); },
        () => { "Entered func C1".Dump(); return new {Name = String.Empty}; },
        () => { "Entered func C2".Dump(); return null; },
    })
    .ForEach(f => {
        var f1 = MemoizationExtensions.Memoize(f);
        Enumerable
            .Range(1,3)
            .Select(i=>new {Run=i, Value=f1()})
            .Dump();
    });
Vous devrez inclure la classe Memoization extensions dans le code du script LinqPad sinon ça ne marchera pas!

1
répondu Norman H 2013-01-16 15:28:01