web-dev-qa-db-ja.com

より効率的なものは何ですか?Dictionary TryGetValueまたはContainsKey + Item?

MSDNのエントリから Dictionary.TryGetValue Method

このメソッドは、ContainsKeyメソッドの機能とItemプロパティを組み合わせたものです。

キーが見つからない場合、valueパラメータは値型TValueの適切なデフォルト値を取得します。たとえば、整数型の場合は0(ゼロ)、ブール型の場合はfalse、参照型の場合はnullです。

コードが辞書にないキーに頻繁にアクセスしようとする場合は、TryGetValueメソッドを使用してください。このメソッドを使用すると、ItemプロパティによってスローされるKeyNotFoundExceptionをキャッチするよりも効率的です。

このメソッドはO(1)操作にアプローチします。

説明から、ContainsKeyを呼び出してルックアップを行うよりも効率的であるか、単に便利であるかは明確ではありません。 TryGetValueの実装は単にContainsKeyを呼び出してからItemを呼び出すのですか、それとも実際には単一のルックアップを実行するよりも効率的ですか?

言い換えれば、より効率的なもの(すなわち、どちらがより少ないルックアップを実行するか):

Dictionary<int,int> dict;
//...//
int ival;
if(dict.ContainsKey(ikey))
{
  ival = dict[ikey];
}
else
{
  ival = default(int);
}

または

Dictionary<int,int> dict;
//...//
int ival;
dict.TryGetValue(ikey, out ival);

注:私はベンチマークを探していません!

228
Rado

TryGetValueは速くなります。

ContainsKeyは、実際のエントリの場所を内部的に参照するTryGetValueと同じチェックを使用します。 Itemプロパティは実際にはTryGetValueとほぼ同じコード機能を持ちますが、falseを返す代わりに例外をスローする点が異なります。

ContainsKeyに続けてItemを使用すると、基本的に検索機能が重複します。これは、この場合の計算の大部分です。

281
Reed Copsey

簡単なベンチマークはTryGetValueがわずかにEdgeを持っていることを示しています。

    static void Main() {
        var d = new Dictionary<string, string> {{"a", "b"}};
        var start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (!d.TryGetValue("a", out x)) throw new ApplicationException("Oops");
            if (d.TryGetValue("b", out x)) throw new ApplicationException("Oops");
        }
        Console.WriteLine(DateTime.Now-start);
        start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (d.ContainsKey("a")) {
                x = d["a"];
            } else {
                x = default(string);
            }
            if (d.ContainsKey("b")) {
                x = d["b"];
            } else {
                x = default(string);
            }
        }
   }

これは作り出します

00:00:00.7600000
00:00:01.0610000

ヒットとミスの均等なブレンドを想定すると、ContainsKey + Itemアクセスを約40%遅くします。

さらに、プログラムを常に見逃すように変更すると(つまり、常に"b"を調べると)、2つのバージョンが同じくらい速くなります。

00:00:00.2850000
00:00:00.2720000

しかし、私がそれを「すべてのヒット」にしたとき、TryGetValueは明らかに勝者です。

00:00:00.4930000
00:00:00.8110000
85
dasblinkenlight

これまでのところ答えはどれも実際には質問に答えていないので、ここで私はいくつかの研究の後に見つけた許容できる答えです:

あなたがTryGetValueを逆コンパイルするならば、あなたはそれがこれをしているのを見る:

public bool TryGetValue(TKey key, out TValue value)
{
  int index = this.FindEntry(key);
  if (index >= 0)
  {
    value = this.entries[index].value;
    return true;
  }
  value = default(TValue);
  return false;
}

一方、ContainsKeyメソッドは次のとおりです。

public bool ContainsKey(TKey key)
{
  return (this.FindEntry(key) >= 0);
}

そのため、TryGetValueは、ContainsKeyにアイテムが存在する場合は配列ルックアップを加えたものです。

出典

TryGetValueは、ContainsKey + Itemの組み合わせのほぼ2倍の速さになります。

46
Rado

誰も気にしない :-)

TryGetValueは使うのが面倒なのでたぶん聞いているでしょう - それでは拡張メソッドでこのようにカプセル化してください。

public static class CollectionUtils
{
    // my original method
    // public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dic, K key)
    // {
    //    V ret;
    //    bool found = dic.TryGetValue(key, out ret);
    //    if (found)
    //    {
    //        return ret;
    //    }
    //    return default(V);
    // }


    // EDIT: one of many possible improved versions
    public static TValue GetValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key)
    {
        // initialized to default value (such as 0 or null depending upon type of TValue)
        TValue value;  

        // attempt to get the value of the key from the dictionary
        dictionary.TryGetValue(key, out value);
        return value;
    }

それからちょうど呼んでください:

dict.GetValueOrDefault("keyname")

または

(dict.GetValueOrDefault("keyname") ?? fallbackValue) 
18
Simon_Weaver

これまでのすべての答えは、優れていますが、重要な点を見逃しています。

API(例えば、.NETフレームワーク)のクラスへのメソッドは、インターフェース定義の一部を形成する(C#またはVBインターフェースではなく、コンピュータサイエンスの意味でのインターフェース)。

そのため、そのようなメソッドを呼び出すほうが速いかどうかを尋ねるのは通常間違っています。ただし、speedが正式なインタフェース定義の一部でない場合(この場合はそうではありません)。

従来、この種のショートカット(検索と取得を組み合わせたもの)は、言語、インフラストラクチャ、OS、プラットフォーム、またはマシンアーキテクチャに関係なく、より効率的です。また、(コードの構造から)暗示するのではなく、明示的にあなたの意図を表現しているので、より読みやすくなります。

そのため、答えは(グリズリーされた古いハックからの)間違いなく 'Yes'です(TryGetValueは、辞書から値を取得するためにContainsKeyとItem [Get]の組み合わせよりも望ましいです)。

これが奇妙に聞こえると思うならば、このように考えてください:TryGetValue、ContainsKey、およびItem [Get]の現在の実装が速度差を生じないとしても、将来の実装(例えば.NET v5)であると考えることができます。する(TryGetValueが速くなります)。あなたのソフトウェアの寿命について考えてください。

余談ですが、現代の典型的なインターフェース定義テクノロジがタイミング制約を正式に定義するための手段を提供することはめったにありません。多分.NET v5?

10
debater

どうしてテストしないの?

しかし、私はTryGetValueがより速いと確信しています、なぜならそれは1つの検索だけをするからです。もちろんこれは保証されていません。つまり、実装が異なればパフォーマンス特性も異なる可能性があります。

辞書を実装する方法は、アイテムのスロットを見つける内部的なFind関数を作成し、それを基に残りを構築することです。

10
CodesInChaos

クイックテストプログラムを作成すると、TryGetValueを使用して辞書に100万の項目を含めることで、間違いなく改善が見られます。

結果:

ContainsKey +アイテム1000000ヒット数:45ms

100万ヒットに対するTryGetValue:26ms

これがテストアプリです。

static void Main(string[] args)
{
    const int size = 1000000;

    var dict = new Dictionary<int, string>();

    for (int i = 0; i < size; i++)
    {
        dict.Add(i, i.ToString());
    }

    var sw = new Stopwatch();
    string result;

    sw.Start();

    for (int i = 0; i < size; i++)
    {
        if (dict.ContainsKey(i))
            result = dict[i];
    }

    sw.Stop();
    Console.WriteLine("ContainsKey + Item for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();

    for (int i = 0; i < size; i++)
    {
        dict.TryGetValue(i, out result);
    }

    sw.Stop();
    Console.WriteLine("TryGetValue for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

}
5
davisoa

私のマシンでは、(DEBUGではなく)RELEASEモードで実行したときに、try-catch内のすべてのエントリが見つかった場合、ContainsKeyTryGetValue/Dictionary<>と等しくなります。

見つからない辞書エントリがいくつかある場合は、ContainsKeyのほうがはるかに優れています(以下の例では、MAXVALENTRIESよりも大きい値に設定すると、一部のエントリが失われます)。

結果:

Finished evaluation .... Time distribution:
Size: 000010: TryGetValue: 53,24%, ContainsKey: 1,74%, try-catch: 45,01% - Total: 2.006,00
Size: 000020: TryGetValue: 37,66%, ContainsKey: 0,53%, try-catch: 61,81% - Total: 2.443,00
Size: 000040: TryGetValue: 22,02%, ContainsKey: 0,73%, try-catch: 77,25% - Total: 7.147,00
Size: 000080: TryGetValue: 31,46%, ContainsKey: 0,42%, try-catch: 68,12% - Total: 17.793,00
Size: 000160: TryGetValue: 33,66%, ContainsKey: 0,37%, try-catch: 65,97% - Total: 36.840,00
Size: 000320: TryGetValue: 34,53%, ContainsKey: 0,39%, try-catch: 65,09% - Total: 71.059,00
Size: 000640: TryGetValue: 32,91%, ContainsKey: 0,32%, try-catch: 66,77% - Total: 141.789,00
Size: 001280: TryGetValue: 39,02%, ContainsKey: 0,35%, try-catch: 60,64% - Total: 244.657,00
Size: 002560: TryGetValue: 35,48%, ContainsKey: 0,19%, try-catch: 64,33% - Total: 420.121,00
Size: 005120: TryGetValue: 43,41%, ContainsKey: 0,24%, try-catch: 56,34% - Total: 625.969,00
Size: 010240: TryGetValue: 29,64%, ContainsKey: 0,61%, try-catch: 69,75% - Total: 1.197.242,00
Size: 020480: TryGetValue: 35,14%, ContainsKey: 0,53%, try-catch: 64,33% - Total: 2.405.821,00
Size: 040960: TryGetValue: 37,28%, ContainsKey: 0,24%, try-catch: 62,48% - Total: 4.200.839,00
Size: 081920: TryGetValue: 29,68%, ContainsKey: 0,54%, try-catch: 69,77% - Total: 8.980.230,00

これが私のコードです:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;

    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                const int ENTRIES = 10000, MAXVAL = 15000, TRIALS = 100000, MULTIPLIER = 2;
                Dictionary<int, int> values = new Dictionary<int, int>();
                Random r = new Random();
                int[] lookups = new int[TRIALS];
                int val;
                List<Tuple<long, long, long>> durations = new List<Tuple<long, long, long>>(8);

                for (int i = 0;i < ENTRIES;++i) try
                    {
                        values.Add(r.Next(MAXVAL), r.Next());
                    }
                    catch { --i; }

                for (int i = 0;i < TRIALS;++i) lookups[i] = r.Next(MAXVAL);

                Stopwatch sw = new Stopwatch();
                ConsoleColor bu = Console.ForegroundColor;

                for (int size = 10;size <= TRIALS;size *= MULTIPLIER)
                {
                    long a, b, c;

                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine("Loop size: {0}", size);
                    Console.ForegroundColor = bu;

                    // ---------------------------------------------------------------------
                    sw.Start();
                    for (int i = 0;i < size;++i) values.TryGetValue(lookups[i], out val);
                    sw.Stop();
                    Console.WriteLine("TryGetValue: {0}", a = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i) val = values.ContainsKey(lookups[i]) ? values[lookups[i]] : default(int);
                    sw.Stop();
                    Console.WriteLine("ContainsKey: {0}", b = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i)
                        try { val = values[lookups[i]]; }
                        catch { }
                    sw.Stop();
                    Console.WriteLine("try-catch: {0}", c = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    Console.WriteLine();

                    durations.Add(new Tuple<long, long, long>(a, b, c));
                }

                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Finished evaluation .... Time distribution:");
                Console.ForegroundColor = bu;

                val = 10;
                foreach (Tuple<long, long, long> d in durations)
                {
                    long sum = d.Item1 + d.Item2 + d.Item3;

                    Console.WriteLine("Size: {0:D6}:", val);
                    Console.WriteLine("TryGetValue: {0:P2}, ContainsKey: {1:P2}, try-catch: {2:P2} - Total: {3:N}", (decimal)d.Item1 / sum, (decimal)d.Item2 / sum, (decimal)d.Item3 / sum, sum);
                    val *= MULTIPLIER;
                }

                Console.WriteLine();
            }
        }
    }
4
AxD

実用的な設定で正確な結果をもたらすマイクロベンチマークを設計することとは別に、.NET Frameworkの参照元を調べることができます。

それらのすべてが FindEntry(TKey) メソッドを呼び出しますが、ほとんどの作業を行い、結果を記憶しません。したがって、TryGetValueの呼び出しは、ContainsKey + Itemのほぼ2倍の速さです。


TryGetValueの不便なインターフェースは、拡張メソッドを使用して 調整することができます

using System.Collections.Generic;

namespace Project.Common.Extensions
{
    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault<TKey, TValue>(
            this IDictionary<TKey, TValue> dictionary,
            TKey key,
            TValue defaultValue = default(TValue))
        {
            if (dictionary.TryGetValue(key, out TValue value))
            {
                return value;
            }
            return defaultValue;
        }
    }
}

C#7.1以降、default(TValue)をプレーンなdefaultに置き換えることができます。 型は推測されます。

使用法:

var dict = new Dictionary<string, string>();
string val = dict.GetValueOrDefault("theKey", "value used if theKey is not found in dict");

明示的なデフォルト値が指定されていない限り、検索が失敗した参照型に対してはnullを返します。

var dictObj = new Dictionary<string, object>();
object valObj = dictObj.GetValueOrDefault("nonexistent");
Debug.Assert(valObj == null);

val dictInt = new Dictionary<string, int>();
int valInt = dictInt.GetValueOrDefault("nonexistent");
Debug.Assert(valInt == 0);
2
Palec

辞書から値を取得しようとしている場合、TryGetValue(key、out value)が最良の選択肢ですが、古いキーを上書きせずに新しい挿入のためにキーの存在をチェックしている場合は、その範囲内でのみ、ContainsKey(key)が最良の選択肢であり、ベンチマークはこれを確認できます。

using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections;

namespace benchmark
{
class Program
{
    public static Random m_Rand = new Random();
    public static Dictionary<int, int> testdict = new Dictionary<int, int>();
    public static Hashtable testhash = new Hashtable();

    public static void Main(string[] args)
    {
        Console.WriteLine("Adding elements into hashtable...");
        Stopwatch watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testhash[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);
        Console.WriteLine("Adding elements into dictionary...");
        watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testdict[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);

        Console.WriteLine("Finding the first free number for insertion");
        Console.WriteLine("First method: ContainsKey");
        watch = Stopwatch.StartNew();
        int intero=0;
        while (testdict.ContainsKey(intero))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Second method: TryGetValue");
        watch = Stopwatch.StartNew();
        intero=0;
        int result=0;
        while(testdict.TryGetValue(intero, out result))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Test hashtable");
        watch = Stopwatch.StartNew();
        intero=0;
        while(testhash.Contains(intero))
        {
            intero++;
        }
        testhash.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} into hashtable -- pause....", watch.Elapsed.TotalSeconds, intero);
        Console.Write("Press any key to continue . . . ");
        Console.ReadKey(true);
    }
}
}

これは本当の例です、私は作成されたそれぞれの "Item"に対してプログレッシブ番号を関連付けるサービスを持っています、この番号は、あなたが新しい項目を作成するたびに、解放されなければなりません。 free、もちろんこれは最適化されていません、私は現在の数をキャッシュする静的変数を持っていますが、あなたがすべての数を終わらせた場合、あなたは0からUInt32.MaxValueまでやり直すことができます

テスト実行:
ハッシュテーブルに要素を追加しています...
0,5908で完了 - 一時停止中....
辞書に要素を追加しています...
0,2679で完了 - 一時停止中....
挿入用の最初の空き番号を見つける
最初の方法:ContainsKey
0,0561で完了 - 辞書に値1000000を追加 - 一時停止....
2番目の方法:TryGetValue
0,0643で完了 - 辞書に追加された値1000001 - 一時停止....
ハッシュテーブルのテスト
0,3015で行われました - ハッシュテーブルに値1000000を追加しました - 一時停止....
何かキーを押すと続行します 。 。

あなたの何人かがContainsKeysに利点があるかどうかを尋ねているかもしれないならば、私もTryGetValueをContainsキーで反転させようと試みました、結果は同じです。

したがって、私にとっては、最後に検討すると、それはすべてプログラムの動作方法によって異なります。

2
Fwiffo