2013-08-27 15 views
9

Używam słownika do gromadzenia liczby wystąpień kluczy, a zatem podstawowa operacja polega na zapisaniu pary klucz-wartość, w której wartość jest poprzednią wartością plus jeden lub tylko jeden, jeśli nie było poprzedniej wartości. Wymaga to jednak dwóch oddzielnych operacji słownikowych (odczyt i zapis), kiedy mógłbym po prostu wykonywać jedną operację (AddOrUpdate).Wydajne aktualizowanie powiązań w słowniku .NET

Zauważam, że słownik współbieżny obsługuje AddOrUpdate, ale zwykły rodzajowy Dictionary nie jest wyświetlany.

W związku z tym słownik odniesień do zmiennych int jest szybszy. Wprowadza to jednak niepotrzebne odniesienia, które oznaczają alokacje sterty i bariery zapisu. Zgaduję, że można zrobić znacznie lepiej, ale nie widzę, jak to zrobić bez przepisywania od podstaw Dictionary. Czy mam rację?

+0

Próbujesz wyeliminować jedno z wyszukiwań w scenariuszu dodawania lub aktualizacji? – mydogisbox

+0

Równoczesny słownik wydaje się dość wydajny w wielu przypadkach, czy sprawdziłeś, czy zapewnia on wystarczającą wydajność dla twojego scenariusza? – Alex

+0

Czy możesz posortować pary klucz-wartość? Domyślam się, że większość będzie O (n log n), więc być może będziesz musiał przetestować najlepszą wydajność – Carsten

Odpowiedz

2

Słownik aktualizacja nie wymaga wiele wyszukiwań jeśli używasz typów referencyjnych:

że masz Dictionary<string, Foo>, gdzie Foo jest typem odniesienia i obejmuje Count właściwość:

void UpdateCount(string key) 
{ 
    Foo f; 
    if (dict.TryGetValue(key, out f)) 
    { 
     // do the update 
     ++f.Count; 
    } 
    else 
    { 
     dict[key] = 1; 
    } 
} 

If twoje wartości są typami wartości ... cóż, wtedy musisz zajmować się semantyką typu wartości. A to obejmuje wykonanie dwóch wyszukiwań.

To powiedziawszy, wyszukiwanie w słowniku jest dość szybkie. Jeśli to powoduje problem, musisz liczyć się z wieloma wystąpieniami.

3

Jak wspomniał Jim Mischel - nie można wykonać pojedynczego wyszukiwania w celu zmiany wartości elementu słownika. ConcurrentDictionary.AddOrUpdate sposób zrobić więcej niż jedną operację odnośnika (odbicie źródła):

public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory) 
{ 
    TValue local2; 
    if (key == null) 
    { 
     throw new ArgumentNullException("key"); 
    } 
    if (updateValueFactory == null) 
    { 
     throw new ArgumentNullException("updateValueFactory"); 
    } 
    do 
    { 
     TValue local3; 
     while (this.TryGetValue(key, out local3)) 
     { 
      TValue newValue = updateValueFactory(key, local3); 
      if (this.TryUpdate(key, newValue, local3)) 
      { 
       return newValue; 
      } 
     } 
    } 
    while (!this.TryAddInternal(key, addValue, false, true, out local2)); 
    return local2; 
} 

zrobiłem testu wydajności przy jednoczesnym słownika i prosty ditcionary:

przedłużenie AddOrUpdate dla IDictionary:

public static class DictionaryExtensions 
{ 
    public static void AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue initValue, Func<TKey, TValue, TValue> updateFunc) 
    { 
     TValue value; 
     value = dict.TryGetValue(key, out value) ? updateFunc(key, value) : initValue; 

     dict[key] = value; 
    } 
} 

Test:

static void Main(string[] args) 
{ 
    const int dictLength = 100000; 
    const int testCount = 1000000; 

    var cdict = new ConcurrentDictionary<string, int>(GetRandomData(dictLength)); 
    var dict = GetRandomData(dictLength).ToDictionary(x => x.Key, x => x.Value); 

    var stopwatch = new Stopwatch(); 
    stopwatch.Start(); 
    foreach (var pair in GetRandomData(testCount)) 
     cdict.AddOrUpdate(pair.Key, 1, (x, y) => y+1);   

    stopwatch.Stop(); 
    Console.WriteLine("Concurrent dictionary: {0}", stopwatch.ElapsedMilliseconds); 

    stopwatch.Reset(); 
    stopwatch.Start(); 

    foreach (var pair in GetRandomData(testCount)) 
     dict.AddOrUpdate(pair.Key, 1, (x, y) => y+1); 

    stopwatch.Stop(); 
    Console.WriteLine("Dictionary: {0}", stopwatch.ElapsedMilliseconds); 
    Console.ReadLine(); 
} 

static IEnumerable<KeyValuePair<string, int>> GetRandomData(int count) 
{ 
    const int constSeed = 100; 
    var randGenerator = new Random(constSeed); 
    return Enumerable.Range(0, count).Select((x, ind) => new KeyValuePair<string, int>(randGenerator.Next().ToString() + "_" + ind, randGenerator.Next())); 
} 

Wyniki badań moim otoczeniu (MS):

ConcurrentDictionary: 2504 
Dictionary: 1351 
5

można zrobić coś takiego:

private class Counter 
{ 
    public string Key  { get ; set ; } 
    public int Frequency { get ; set ; } 
} 

... 

Dictionary<string,Counter> frequencyTable = new Dictionary<string,Counter>() ; 

... 

string someKey = GetKeyToLookup() ; 
Counter item = null ; 
bool hit = frequencyTable.TryGetValue(someKey,out item) ; 
if (!hit) 
{ 
    item = new Counter{ Key=someKey,Frequency=0 } ; 
} 
++ item.Frequency ; 

Jeśli to nie jest wystarczająco dobre, to dlaczego napisać własny? Użyj wysokiej wydajności C5 Collections Library. Jest darmowy (początkowo finansowany przez Microsoft), bazuje na interfejsach Microsoft System.Collections.Generic, a ich słowniki, zestawy i torby obsługują semantykę FindOrAdd().

+0

Tak, to jest dokładnie to, co rozumiem przez "słownik odniesień do zmiennych int jest szybszy", ale wprowadza niepotrzebne odniesienia, które oznaczają alokacje sterty i pisanie barier. –

+0

@JonHarrop Czy próbowałeś? Czy C5 jest rzeczywiście bardziej wydajny w tym zadaniu? Czy drugie wyszukiwanie lub typ referencyjny jest bardziej kosztowny? – Goswin

+0

Próbowałem go z własnym kodem (nie C5), a słownik odwołań zmiennych był szybszy niż podwójne wyszukiwania w słowniku wartości. Drugie wyszukiwanie jest droższe. Jednak słownik, który pozwala na dodanie miejsca jest najszybszym rozwiązaniem, oczywiście. –