2014-01-29 6 views
6

Ich möchte eine Echtzeit-Score-Ranking (bestellt) implementieren. Ich hoffe, dass ich die Punktzahl aller Spieler schnell erreichen kann (Schlüssel-Wert).Erlang bestellt und Schlüssel-Wert-Datenstruktur

Hier player_id ist der Schlüssel und Score ist Wert.

Ich versuche, ETS zu verwenden, um die Liste aller Spielerscore sortiert zu bestellen, aber sortierte Reihenfolge nach dem Schlüssel nicht den Wert.

Hat Erlang/OTP eine andere Datenstruktur, die mein Problem lösen kann?

+0

Werfen Sie einen Blick auf Oddict http://erdocs.com/R15B/stdlib/orddict.html. Ich habe nicht überprüft, ob es die Funktionalität unterstützt, die Sie benötigen, aber es sieht so aus, Sie können einen Wrapper erstellen, um Ihre Funktionalität zu implementieren – Arunmu

+0

@ArunMu Es tut mir leid, es scheint orddict "Die Liste ist nach den Schlüsseln geordnet". Ich möchte nach dem Wert – mingchaoyan

+0

ordere Ich bin nur neugierig zu wissen, warum Sie nach Wert sortieren möchten. Wie hilft es beim Abrufen der Schlüssel/Wert-Paare? – Arunmu

Antwort

4

Ich wäre nicht wählerisch über die Art und Weise, wie Erlang seine Daten bestellt: es optimiert sich selbst oder schnelle Lookups. Was Sie jedoch tun könnten, ist Ihre ETS-Tabelle in einer Liste zu lesen, und verwenden Sie lists:sort/2, um die Daten zu sortieren.

Es wird immer noch schnell sein: ETS-Tabellen und -Listen befinden sich im Speicher, solange Sie genug davon haben.

Dieses Modul ist eine Schnittstelle zum Erlang Einbau-Zeitlagerung BIFs: Allerdings habe ich die ordered_set Dump würde, werden Sie die konstante Zugriffszeit

From the ETS manual verlieren. Diese bieten die Möglichkeit, sehr große Datenmengen in einem Erlang-Laufzeitsystem zu speichern und eine konstante Zugriffszeit auf die Daten zu haben. (Im Fall von ordered_set, siehe unten, Zugriffszeit ist proportional zu der Logarithmus der Anzahl der gespeicherten Objekte).

Und vergessen Sie nicht irgendeine Form von Disk-basierten Backup, Dets oder Mnesia (wenn die Daten wert sind, zu halten).

+0

Es tut mir leid. Der Wert ändert sich sehr häufig, daher hoffe ich, den Schlüsselwert bei Änderung (O (n)) einzugeben/zu aktualisieren und den Wert bei Bedarf zu erhalten (O (logn)). Aber in Ihrem Fall muss ich 1) tab2list 2) sort 3) list2tab (O (n * logn)). – mingchaoyan

+0

Nun, wenn Geschwindigkeit so wichtig ist, warum gehst du dann nicht rein C? Erlang ist sowieso nicht außergewöhnlich schnell. Niemals dazu gedacht. http://stackoverflow.com/questions/6964392/speed-comparison-with-project-euler-c-vs-python-vs-erlang-vs-haskell – Berzemus

+0

Oder Sie könnten Ihr eigenes Binärformat verwenden und Erlangs Bit-Syntax verwenden um es effektiv zu verwalten. Wäre immer noch langsamer als einige intelligente Speicherverwaltung/Hacking in C obwohl. – Berzemus

6

Was ich verstehe, ist, dass Sie eine Liste des Paares (Key, Score), mit dem Sie ausführen möchten halten:

  • häufige Aktualisierung der Partitur,
  • häufig einen vollständigen oder teilweise angezeigt werden Ansicht der Liste nach Punkten sortiert

ich Ihnen vorschlagen, speichern Sie Ihre Daten in 2 verschiedenen ets:

  • Der erste für den schnellen Zugriff auf den Schlüssel ist ein Satz, in dem Sie den Schlüssel im ersten Feld und den Wert im zweiten Feld speichern.
  • Die zweite ist eine geordnete Menge, in der Sie ein Tupel {Score, Key} als Schlüssel und keinen Wert speichern. Dies sollte die Eindeutigkeit jedes Datensatzes garantieren und die Liste nach Punkten geordnet halten.

Wenn Sie Noten anzeigen müssen, ist die geordnete Menge ausreichend.

Wenn Sie eine Partitur aktualisieren müssen, sollten Sie die Ets verwenden, um den Wert der vorherigen Partitur für Key zu erhalten, den Datensatz {PrevScore, Key} löschen und {NewScore, Key} in die geordnete Menge einfügen und einfach die Erste Ets mit neuem Wert.

Ich testete diese Lösung auf einer 1 000 000 Artikel Liste, das Update von 1 Score dauert durchschnittlich 3 μs auf meinem Laptop (Windows XP, Core i5, 2Go RAM, alle Festplatten voll und viele App im Hintergrund laufen). I der verwendete Code:

note Ich benutzte private Tische und einen einzelnen Server sie zuzugreifen, um die Konsistenz der zwei Tabellen, so gleichzeitige Prozesse zu gewährleisten, kann auf den Server zugreifen (namens score) ohne Konflikte, die Anforderungen werden in der Reihenfolge ausgeführt, in der sie beim Server ankommen. Es könnte möglich sein, eine Antwort auf jede get (N) Anfrage mit 2 Empfangsblöcken zu erhalten.

hier ist das Testergebnis auf meinem Heim-PC (ubuntu 12.04, 8gb ddr, AMD Phenom II X6) ...

[Bearbeiten] ich das Update/2-Funktion, um synchron zu sein modifiziert, So ist die Maßnahme jetzt signifikant und das Ergebnis verständlicher. Es scheint, dass für eine Tabelle, die kleiner als 10000 Datensätze ist, der Overhead der ETS-Verwaltung und der Nachrichtenweitergabe überwiegt. enter image description here

-module (score). 

-export ([start/0]). 
-export ([update/2,delete/1,get/1,stop/0]). 

score ! {update,M,S,self()}, 
    receive 
     ok -> ok 
    end. 

delete(M) -> 
    score ! {delete,M}. 

get(N) -> 
    score ! {getbest,N,self()}, 
    receive 
     {ok,L} -> L 
    after 5000 -> 
     timeout 
    end. 

stop() -> 
    score ! stop. 


start() -> 
    P = spawn(fun() -> initscore() end), 
    register(score,P). 


initscore() -> 
    ets:new(score,[ordered_set,private,named_table]), 
    ets:new(member,[set,private,named_table]), 
    loop(). 

loop() -> 
    receive 
     {getbest,N,Pid} when is_integer(N), N > 0 -> 
      Pid ! {ok,lists:reverse(getbest(N))}, 
      loop(); 
      {update,M,S,P} -> 
        update_member(M,S), 
        P ! ok, 
      loop(); 
     {delete,M} -> 
      delete_member(M), 
      loop(); 
     stop -> 
      ok 
    end. 



update_member(M,S) -> 
    case ets:lookup(member,M) of 
     [] -> 
      ok; 
     [{M,S1}] -> 
      ets:delete(score,{S1,M}) 
    end, 
    ets:insert(score,{{S,M}}), 
    ets:insert(member,{M,S}). 

delete_member(M) -> 
    case ets:lookup(member,M) of 
     [] -> 
      ok; 
     [{M,S}] -> 
      ets:delete(score,{S,M}), 
      ets:delete(member,M) 
    end. 

getbest(N) -> 
    K= ets:last(score), 
    getbest(N-1,K,[]). 

getbest(_N,'$end_of_table',L) -> L; 
getbest(0,{S,M},L) -> [{M,S}|L]; 
getbest(N,K={S,M},L) -> 
    K1 = ets:prev(score,K), 
    getbest(N-1,K1,[{M,S}|L]). 

und der Code für den Test:

-module (test_score). 

-compile([export_all]). 

init(N) -> 
    score:start(), 
    random:seed(erlang:now()), 
    init(N,10*N). 

stop() -> 
    score:stop(). 

init(0,_) -> ok; 
init(N,M) -> 
    score:update(N,random:uniform(M)), 
    init(N-1,M). 

test_update(N,M) -> 
    test_update(N,M,0). 

test_update(0,_,T) -> T; 
test_update(N,M,T) -> test_update(N-1,M,T+update(random:uniform(M),random:uniform(10*M))). 

update(K,V) -> 
    {R,_} = timer:tc(score,update,[K,V]), 
    R. 
+0

das Maß ist falsch, ich habe die Zeit von der externen Sicht gemessen, und für ein Update ist es nur die Zeit, um eine Nachricht zu senden, ist es, warum es so stabil ist. Ich werde versuchen, später ein neues Maß zu geben ... – Pascal

4

Es gibt drei Lösungen:

  • ets geordneter gesetzt

  • RAM-only Mnesia Tabelle mit sekundären Index

  • nif

1) bestellt-Set, Datensätze in der Tabelle ets sollte {Score, player_id}, {nicht player_id, score}, so dass sie nach Punkten sortiert sind. Um den Punktestand für einen Spieler zu erhalten, benutze einfach match. Obwohl das Match die gesamte Tabelle scannen muss, ist es immer noch ziemlich schnell.

Profiling: unter der Annahme, dass es 10k Spieler gibt, 10k Datensätze in ets Tabelle einfügen, dann ets: match_object (Tabelle, {'_', PlayerID}). Jedes Spiel dauert 0,7 bis 1,1 ms, was in den meisten Fällen mehr als gut genug ist. (CPU: i5 750)

  1. match_object ist etwas schneller als Übereinstimmung;
  2. Wählen Sie mit einer Übereinstimmung Spezifikation ist langsamer als Übereinstimmung, wahrscheinlich, weil die Auswahl hier ganz einfach ist, dass fun2ms Overhead seine Verstärkung überwiegt. Beachten Sie, dass im Allgemeinen die Auswahl Vorrang vor Übereinstimmung hat.

2) Mnesia Tisch, machen es RAM-only und einen Sekundärindex für player_id verwenden:

mnesia:create_table(user, [{type, ordered_set}, {attributes, record_info(fields, user)}, {index, [playerid]}]) 

Durchschnittliche get Zeit mit Mnesia: read in Mnesia: Transaktion ist 0.09ms. Aber das Einfügen von 10k-Datensätzen ist ungefähr 180-mal langsamer als sein ets-Gegenstück (2820ms gegenüber 15ms).

Wenn weder ETS noch MNESIA Ihre Leistungsanforderung erfüllen, könnte der Rückgriff auf C mit NIF eine andere Wahl sein. Aber persönlich denke ich, dass Überoptimierung hier nicht angemessen ist, es sei denn, es ist wirklich dein Flaschenhals.

+0

(1) ist nicht für die gleichen Punkte geeignet. Es wird wegen des gleichen Schlüssels ersetzt. – goofansu

+0

@goofansu ein Workaround wäre es, '{score, player_id}' Tupel als Schlüssel zu verwenden –