26

関数のメモ化に対するWesDyerのアプローチを出発点として考えてみましょう。

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

問題は、複数のスレッドから使​​用すると、問題が発生する可能性があることです。

Func<int, int> f = ...
var f1 = f.Memoize();
...
in thread 1:
var y1 = f1(1);
in thread 2:
var y2 = f1(1);
// We may be recalculating f(1) here!

これを避けてみましょう。ロックオンmap

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, R>();
  return a =>
    {
      R value;
      lock(map) 
      {
        if (map.TryGetValue(a, out value))
          return value;
        value = f(a);
        map.Add(a, value);
      }
        return value;
    };
}

一度f1に多くの異なる引数を計算することができないため、これは明らかに恐ろしい考えです。a値型がある場合、ロックオンは機能しません(制御せず、外部コードもロックする可能性がaあるため、とにかく悪い考えです)。a

これが私が考えることができる2つのオプションです:

Lazy<T>遅延評価用のクラスを想定する(ここを参照):

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, Lazy<R>>();
  return a =>
    {
      Lazy<R> result;
      lock(map) 
      {
        if (!map.TryGetValue(a, out result))
        {  
          result = () => f(a);
          map.Add(a, result);
        }
      }
      return result.Value;
    };
}

または、同期のためにオブジェクトの追加ディクショナリを保持します。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, R>();
  var mapSync = new Dictionary<A, object>();
  return a =>
    {
      R value;
      object sync;
      lock(mapSync)
      { 
        if (!mapSync.TryGetValue(a, out sync))
        { 
          sync = new object();
          mapSync[a] = sync;
        }
      }
      lock(map)
      {
        if (map.TryGetValue(a, out value))
          return value;
      }
      lock(sync)
      {
        value = f(a);
        lock(map)
        {
          map[a] = value;
        }
        return value;
      }
    };
}

より良いオプションはありますか?

4

7 に答える 7

44

ConcurrentDictionary<A, R>不要な.net 4.0 を使用しないでくださいLazy<R>
重要なのはGetOrAdd(A, Func<A, R>)、美しく自明なラムダにレンダリングすることです。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, R>();
    return a => cache.GetOrAdd(a, f);
};

更新上記のソリューションでは、最小限のオーバーヘッドで複数の同時リーダーとライターを使用できます。f(a)ただし、同じ値に対して(計算中の期間中)複数回実行されることは妨げられません。

それが重要な場合は、値をラップできますが、Lazy<R>読み取りごとにコストが発生します。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, Lazy<R>>();
    return a => cache.GetOrAdd(a, new Lazy<R>(() => f(a))).Value;
}

事前設定された 1000 アイテムのキャッシュの 100 万回の読み取りに対する更新ConcurrentDictionaryタイミング テストでは、19 ミリ秒(通常と同じ)Dictionaryが、バージョンでは720 ミリ秒Lazyでした。

それが難しすぎると思われる場合は、より複雑なソリューションを使用して両方の長所を活かすことができます。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, R>();
    var syncMap = new ConcurrentDictionary<A, object>();
    return a =>
    {
        R r;
        if (!cache.TryGetValue(a, out r))
        {
            var sync = syncMap.GetOrAdd(a, new object());
            lock (sync)
            {
                r = cache.GetOrAdd(a, f);
            }
            syncMap.TryRemove(a, out sync);
        }
        return r;
    };
}
于 2012-02-16T23:03:22.193 に答える
10

すでにそのLazy<T>タイプをお持ちの場合は、.net 4.0 を使用していると思われるため、以下も使用できますConcurrentDictionary<A,R>

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new ConcurrentDictionary<A, Lazy<R>>();
  return a =>
    {
      Lazy<R> lazy = new Lazy<R>(() => f(a), LazyExecutionMode.EnsureSingleThreadSafeExecution);
      if(!map.TryAdd(a, lazy))
      {
        return map[a].Value;
      }
      return lazy.Value;
    };
}
于 2009-08-10T14:10:59.587 に答える
2

Thomas の回答は、Lazy コンストラクターへの enum パラメーターが原因で、.NET 4.0 ではコンパイルされないようです。以下に改めました。また、独自の等値比較子を提供するためのオプションのパラメーターも追加しました。これは、TInput が独自の Equals を実装していない場合や、TInput が文字列で大文字と小文字を区別しないようにしたい場合などに便利です。

    public static Func<TInput, TResult> Memoize<TInput, TResult>(
        this Func<TInput, TResult> func, IEqualityComparer<TInput> comparer = null)
    {
        var map = comparer == null
                      ? new ConcurrentDictionary<TInput, Lazy<TResult>>()
                      : new ConcurrentDictionary<TInput, Lazy<TResult>>(comparer);

        return input =>
               {
                   var lazy = new Lazy<TResult>(() => func(input), LazyThreadSafetyMode.ExecutionAndPublication);

                   return map.TryAdd(input, lazy)
                              ? lazy.Value
                              : map[input].Value;
               };
    }

これをテストとして使用して、このメソッドのいくつかの基本的なテストを行いました。

    public void TestMemoize()
    {
        Func<int, string> mainFunc = i =>
                                     {
                                         Console.WriteLine("Evaluating " + i);
                                         Thread.Sleep(1000);
                                         return i.ToString();
                                     };

        var memoized = mainFunc.Memoize();

        Parallel.ForEach(
            Enumerable.Range(0, 10),
            i => Parallel.ForEach(Enumerable.Range(0, 10), j => Console.WriteLine(memoized(i))));
    }

正しく動作しているようです。

于 2010-12-13T18:11:56.243 に答える
1

同じ値を 2 回計算することは望ましくなく、多くのスレッドが同時に値を計算したり、値を取得したりできるようにする必要があります。これを行うには、ある種の条件変数ときめ細かなロック システムを使用する必要があります。

これがアイデアです。値が存在しない場合は、値を同期マップに入れます。その値を必要とするスレッドはそれを待ちます。それ以外の場合は、現在の値を取得するだけです。このようにして、マップのロックは、値のクエリと値の返しに最小限に抑えられます。

    public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
    {
        var map = new Dictionary<A, R>();
        var mapSync = new Dictionary<A, object>();
        return a =>
        {
            R value;
            object sync = null;
            bool calc = false;
            bool wait = false;
            lock (map)
            {
                if (!map.TryGetValue(a, out value))
                {
                    //its not in the map
                    if (!mapSync.TryGetValue(a, out sync))
                    {
                        //not currently being created
                        sync = new object();
                        mapSync[a] = sync;
                        calc = true;

                    }
                    else
                    {
                        calc = false;
                        wait = true;
                    }
                }
            }
            if(calc)
            {
                lock (sync)
                {
                    value = f(a);
                    lock (map)
                    {
                        map.Add(a, value);
                        mapSync.Remove(a);
                    }
                    Monitor.PulseAll(sync);
                    return value;
                }
            }
            else if (wait)
            {
                lock (sync)
                {
                    while (!map.TryGetValue(a, out value))
                    {
                        Monitor.Wait(sync);
                    }
                    return value;
                }
            }

            lock (map)
            {
                return map[a];
            }

        };
    }

これは最初の簡単な試みですが、テクニックを実証していると思います。ここでは、速度のために追加のメモリを交換しています。

于 2009-08-10T15:07:20.480 に答える
1

いいえ、それらはより良い選択肢ではありません。

とにかくすぐに評価するので、遅延評価のバージョンは無意味です。同期ディクショナリを使用するバージョンは、マップ ディクショナリを使用する前にロック内で保護していないため、正しく機能しません。

あなたが恐ろしいと呼んだバージョンは、実際には最良の選択肢です。一度に 1 つのスレッドだけがアクセスできるように、マップ ディクショナリをロック内で保護する必要があります。ディクショナリはスレッド セーフではないため、別のスレッドがディクショナリを変更しているときに、あるスレッドからディクショナリを読み取らせると、問題が発生します。

マップ オブジェクトでロックを使用してもマップ オブジェクト自体は保護されないことに注意してください。マップ参照を識別子として使用して、一度に複数のスレッドを保持し、ロック内でコードを実行するだけです。オブジェクトを変更するコードだけでなく、オブジェクトにアクセスするすべてのコードをロック内に配置する必要があります。

于 2009-08-10T14:07:15.613 に答える
0

記事のスレッドセーフに関するDyerのコメントは読みましたか?

おそらく、Memoize をスレッドセーフにする最も簡単な方法は、マップにロックを設定することです。

これにより、メモ化されている関数が、個別の引数のセットごとに 1 回だけ実行されることが保証されます。

私の RoboRally ゲームの例では、実際に関数メモ化を使用して「代理シングルトン」として機能させました。ファクトリ インスタンスごとに 1 つのインスタンスが存在する可能性があるため (ファクトリが静的でない限り)、これは実際にはシングルトンではありません。しかし、それはまさに私が欲しかったものです。

于 2009-08-10T14:20:10.073 に答える