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


251

MSDNのDictionary.TryGetValueメソッドに関するエントリから:

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

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

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

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

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

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

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);

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

回答:


313

TryGetValue より速くなります。

ContainsKeyはと同じチェックを使用しTryGetValueます。これは内部的に実際のエントリの場所を参照します。Itemプロパティは、実際にはほとんど同じコード機能を持っていますTryGetValue、それは偽を返す代わりに例外がスローされますことを除いて、。

を使用し、ContainsKey続けてItem基本的にルックアップ機能を複製します。これは、この場合の計算の大部分です。


2
これはより微妙です:if(dict.ContainsKey(ikey)) dict[ikey]++; else dict.Add(ikey, 0);。しかし、インデクサープロパティのTryGetValueget および setが使用されるので、それはさらに効率的だと思いませんか?
Tim Schmelter、2015年

4
実際に.netソースを参照 することもできます:referencesource.microsoft.com/#mscorlib/system/collections/…TryGetValue、ContainsKey、this []の3つすべてが同じFindEntryメソッドを呼び出して実行していることがわかります同じ作業量で、質問への答え方が異なるだけです。trygetvalueはブール値と値を返し、キーが含まれる場合はtrue / falseのみを返し、this []は値を返すか例外をスローします。
John Gardner、

1
@JohnGardnerはい、これは私が言ったことですが、ContainsKeyを実行してからItemを取得すると、その作業は1xではなく2xになります。
リードコプシー

3
私は完全に同意します:)実際のソースが現在利用可能であることを指摘していました。他の回答などには実際のソースへのリンクはありませんでした:D
John Gardner

1
少し外れたトピックですが、マルチスレッド環境でIDictionaryを介してアクセスしている場合、ContainsKeyを呼び出したときから状態が変化する可能性があるため、常にTryGetValueを使用します(TryGetValueが内部的に正しくロックされる保証はありませんが、おそらくより安全です)
クリスベリー

91

簡単なベンチマークは、それTryGetValueがわずかに有利であることを示しています。

    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

11
もちろん、実際の使用パターンによって異なります。ルックアップにほとんど失敗しない場合は、TryGetValueはるかに先を行くべきです。また... nitpick ... DateTimeは、パフォーマンス測定をキャプチャするための最良の方法ではありません。
Ed S.

4
@EdS。あなたは正しい、TryGetValueさらに先導します。「すべてのヒット」と「すべてのミス」のシナリオを含めるように回答を編集しました。
dasblinkenlight

2
@ルチアーノはあなたがどのように使用したかを説明しますAny-このように:Any(i=>i.Key==key)。その場合、はい、それは辞書の悪い線形検索です。
ウェストン

13
DateTime.Now正確には数ミリ秒しかありません。代わりにStopwatchクラスを使用しSystem.Diagnosticsます(これにより、カバーの下でQueryPerformanceCounterを使用して、はるかに高い精度が提供されます)。使い方も簡単です。
Alastair Maw 2013年

5
AlastairとEdのコメントに加えて、DateTime.Nowは、ユーザーがコンピューターの時刻を更新したとき、タイムゾーンを超えたとき、またはタイムゾーンが変更されたとき(DST、インスタンス)。GPSや携帯電話ネットワークなどの一部の無線サービスが提供する時刻にシステムクロックが同期しているシステムで作業してみてください。DateTime.Nowはあらゆる場所に行き渡り、DateTime.UtcNowはそれらの原因の1つのみを修正します。ストップウォッチを使用してください。
antiduh 2014

51

これまでのところ、どの答えも実際には質問に答えていないので、いくつかの調査の後に私が見つけた許容できる答えは次のとおりです。

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倍の速さであるようです。


20

誰も気にしない :-)

使用するのが面倒なので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) 

1
@Hüseyinこれを投稿せずに投稿するのに十分なほど愚かだったので非常に混乱thisしましたが、コードベースでメソッドが2回複製されていることがわかりthisました。修正してくれてありがとう!
Simon_Weaver

2
TryGetValueキーが存在しない場合、out値パラメーターにデフォルト値を割り当てます。これにより、これを簡略化できます。
Raphael Smit

2
簡易バージョン:public static TValue GetValueOrDefault <TKey、TValue>(this Dictionary <TKey、TValue> dict、TKey key){TValue ret; dict.TryGetValue(key、out ret); retを返す; }
ジョシュア

2
C#7で、これは本当に楽しいです:if(!dic.TryGetValue(key, out value item)) item = dic[key] = new Item();
シミーWeitzhandler

1
皮肉なことに、実際のソースコードは、すでにGetValueOrDefault()ルーチンを持っていますが、それは隠されています... referencesource.microsoft.com/#mscorlib/system/collections/...
DEVEN T. Corzine

10

テストしてみませんか?

しかしTryGetValue、1つのルックアップしか行わないので、私はそれがより速いと確信しています。もちろん、これは保証されていません。つまり、実装ごとにパフォーマンス特性が異なる可能性があります。

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


実装の詳細によって、アクションXを1回実行する方がアクションXを2回実行するよりも高速または同等であるという保証が変わる可能性はないと思います。最良の場合、それらは同一で、最悪の場合、2Xバージョンには2倍の時間がかかります。
Dan Bechard 2017年

9

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

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

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

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

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

これが奇妙に聞こえると思う場合は、次のように考えてください。TryGetValue、ContainsKey、およびItem [Get]の現在の実装で速度の違いが生じない場合でも、将来の実装(.NET v5など)である可能性が高いと想定できます。実行します(TryGetValueが高速になります)。ソフトウェアの寿命について考えます。

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


2
セマンティクスについてのあなたの主張には100%同意しますが、パフォーマンステストを行う価値はあります。テストを行わない限り、使用しているAPIの実装が最適ではないため、意味的に正しい処理が遅くなることはありません。
Dan Bechard 2017年

5

簡単なテストプログラムを作成すると、間違いなく、辞書に100万個の項目があるTryGetValueを使用して改善されます。

結果:

ContainsKey + 1000000ヒットのアイテム:45ミリ秒

1000000ヒットのTryGetValue:26ミリ秒

ここにテストアプリがあります:

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

私のマシンでは、RAMがロードされており、RELEASEモード(DEBUGでContainsKeyはない)で実行すると、TryGetValue/にtry-catchすべてのエントリDictionary<>が見つかった場合に等しくなります。

ContainsKey見つからないディクショナリエントリがほんの数個ある場合、それらをはるかに上回ります(以下の私の例では、一部のエントリが欠落するMAXVALよりも大きい値に設定しENTRIESます)。

結果:

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();
            }
        }
    }

ここで何か怪しいことが起こっているような気がします。取得した値を使用しないため、オプティマイザがContainsKey()チェックを削除または簡略化しているのではないかと思います。
Dan Bechard 2017年

できません。ContainsKey()はコンパイル済みDLLにあります。オプティマイザーは、ContainsKey()が実際に行うことについて何も知りません。副作用を引き起こす可能性があるため、呼び出す必要があり、省略できません。
AxD 2017年

ここで何かが偽物です。実際のところ、.NETコードを調べると、ContainsKey、TryGetValue、およびthis []はすべて同じ内部コードを呼び出しているため、エントリが存在する場合、TryGetValueはContainsKey + this []よりも高速です。
ジムバルター2017

3

実用的な設定で正確な結果が得られるマイクロベンチマークを設計する以外に、.NET Frameworkの参照ソースを検査できます。

それらはすべて、FindEntry(TKey)ほとんどの処理を実行し、その結果をメモしないメソッドを呼び出すTryGetValueContainsKeyItemため、呼び出し+のほぼ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)、plainに置き換えることができます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);

拡張メソッドのユーザーは、存在しないキーと存在するキーの違いを見分けることができないが、その値はdefault(T)であることに注意してください。
Lucas

最近のコンピューターでは、サブルーチンを2回続けて呼び出すと、1回の呼び出しの2倍の時間がかかることはほとんどありません。これは、CPUとキャッシュアーキテクチャが最初の呼び出しに関連付けられた多くの命令とデータをキャッシュする可能性が非常に高いため、2番目の呼び出しがより高速に実行されるためです。一方、2回の呼び出しは1回の呼び出しよりも少し長くかかることがほぼ確実であるため、可能であれば2番目の呼び出しを削除することには利点があります。
討論者

2

辞書から値を取得しようとしている場合は、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);
    }
}
}

これは本当の例です。作成した「アイテム」ごとにプログレッシブ番号を関連付けるサービスがあります。この番号は、新しいアイテムを作成するたびに無料で見つける必要があります。アイテムを削除すると、無料の番号は無料、もちろんこれは最適化されていません。現在の数値をキャッシュする静的変数があるためですが、すべての数値を終了する場合は、0からUInt32.MaxValueに再開始できます。

テストの実行:
ハッシュテーブルへの要素の追加...
0,5908で完了-一時停止...
辞書への要素の追加...
0,2679で完了-一時停止...
挿入する最初の空き番号を見つける
最初のメソッド:ContainsKey
Done in 0,0561-値1000000をディクショナリに追加-一時停止...
2番目のメソッド:TryGetValue
Done in 0,0643-値1000001をディクショナリに追加-一時停止...
ハッシュテーブルのテスト
0で完了3015-ハッシュテーブルに値1000000を追加-一時停止...
任意のキーを押して続行します。。

ContainsKeysに利点があるかどうかを尋ねる人がいるかもしれませんが、Containsキーを使用してTryGetValueを逆にしてみても、結果は同じです。

したがって、私にとっては、最後の考慮事項として、すべてはプログラムの動作に依存します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.