Gibt es einen Vorteil der Verwendung von map gegenüber unordered_map bei trivialen Schlüsseln?


371

Ein kürzlich unordered_mapin C ++ veröffentlichter Vortrag hat mir klar gemacht, dass ich ihn aufgrund der Effizienz der Suche ( amortisiertes O (1) vs. O (log n) ) unordered_mapfür die meisten Fälle verwenden sollte, in denen ich ihn mapzuvor verwendet habe . Meistens verwende ich eine Karte, entweder oder als Schlüsseltyp. Daher habe ich keine Probleme mit der Definition der Hash-Funktion. Je mehr ich darüber nachdachte, desto mehr wurde mir klar, dass ich bei Schlüsseln mit einfachen Typen keinen Grund finden kann, ein Over-A zu verwenden. Ich habe mir die Schnittstellen angesehen und keine gefunden signifikante Unterschiede, die sich auf meinen Code auswirken würden.intstd::stringstd::mapstd::unordered_map

Daraus ergibt sich die Frage: Gibt es eine wahre Grund für die Verwendung std::mapüber std::unordered_mapim Fall von einfachen Typen wie intund std::string?

Ich frage aus rein programmtechnischer Sicht - ich weiß, dass es nicht vollständig als Standard betrachtet wird und dass es Probleme mit der Portierung geben kann.

Außerdem erwarte ich, dass eine der richtigen Antworten "es ist effizienter für kleinere Datensätze" aufgrund eines geringeren Overheads ist (stimmt das?) - daher möchte ich die Frage auf Fälle beschränken, in denen die Menge von Schlüssel sind nicht trivial (> 1 024).

Edit: duh, ich habe das Offensichtliche vergessen (danke GMan!) - ja, Karten sind natürlich bestellt - das weiß ich und suche nach anderen Gründen.


22
Ich stelle diese Frage gerne in Interviews: "Wann ist eine schnelle Sortierung besser als eine Blasensortierung?" Die Antwort auf die Frage gibt einen Einblick in die praktische Anwendung der Komplexitätstheorie und nicht nur einfache Schwarz-Weiß-Aussagen wie O (1) sind besser als O (n) oder O (k) sind gleichbedeutend mit O (logn) usw. ..

42
@Beh, ich denke du meintest "wann ist Blasensortierung besser als Schnellsortierung": P
Kornel Kisielewicz

2
Wäre ein intelligenter Zeiger ein trivialer Schlüssel?
thomthom

Hier ist einer der Fälle, in denen die Karte die vorteilhafte ist: stackoverflow.com/questions/51964419/…
anilbey

Antworten:


399

Vergessen Sie nicht, dass mapdie Elemente geordnet bleiben. Wenn Sie das nicht aufgeben können, können Sie es natürlich nicht verwenden unordered_map.

Beachten Sie außerdem, dass im unordered_mapAllgemeinen mehr Speicher benötigt wird. maphat nur ein paar Haushaltszeiger und Speicher für jedes Objekt. Im Gegensatz dazu unordered_maphat ein großes Array (diese können in einigen Implementierungen ziemlich groß werden) und dann zusätzlichen Speicher für jedes Objekt. Wenn Sie speicherbewusst sein müssen, mapsollte sich dies als besser erweisen, da das große Array fehlt.

Wenn Sie also einen reinen Lookup-Retrieval benötigen, unordered_mapist dies der richtige Weg. Aber es gibt immer Kompromisse, und wenn Sie sie sich nicht leisten können, können Sie sie nicht nutzen.

Nur aus persönlicher Erfahrung heraus fand ich eine enorme Verbesserung der Leistung (natürlich gemessen) bei der Verwendung unordered_mapanstelle mapeiner Nachschlagetabelle für Hauptentitäten.

Andererseits stellte ich fest, dass das wiederholte Einfügen und Entfernen von Elementen viel langsamer war. Es ist großartig für eine relativ statische Sammlung von Elementen, aber wenn Sie Tonnen von Einfügungen und Löschungen vornehmen, scheint sich das Hashing + Bucketing zu summieren. (Beachten Sie, dass dies über viele Iterationen hinweg war.)


3
Eine weitere Sache über die große (r) Speicherblock-Eigenschaft von unordered_map vs. map (oder vector vs. list) ist, dass der Standardprozess-Heap (hier Windows) serialisiert wird. Das Zuweisen von (kleinen) Blöcken in großen Mengen in einer Multithread-Anwendung ist sehr teuer.
Brüllen

4
RA: Sie können dies mit Ihrem eigenen Allokatortyp in Kombination mit jedem Container etwas steuern, wenn Sie der Meinung sind, dass dies für ein bestimmtes Programm von Bedeutung ist.

9
Wenn Sie die Größe der kennen unordered_mapund diese zu Beginn reservieren - zahlen Sie trotzdem eine Strafe für viele Einfügungen? Angenommen, Sie fügen nur einmal ein, wenn Sie die Nachschlagetabelle erstellt haben - und lesen später nur noch daraus.
Thomthom

3
@thomthom Soweit ich das beurteilen kann, sollte es keine Leistungseinbußen geben. Der Grund, warum die Leistung beeinträchtigt wird, liegt in der Tatsache, dass das Element, wenn es zu groß wird, alle Elemente erneut aufbereitet. Wenn Sie Reserve aufrufen, werden möglicherweise die vorhandenen Elemente erneut aufbereitet.
Richard Fung

6
Ich bin mir ziemlich sicher, dass es in Bezug auf die Erinnerung das Gegenteil ist. Angenommen, der Standardladefaktor 1,0 für einen ungeordneten Container: Sie haben einen Zeiger pro Element für den Bucket und einen Zeiger pro Element für das nächste Element im Bucket. Daher erhalten Sie zwei Zeiger plus Daten pro Element. Für einen geordneten Container hingegen hat eine typische RB-Baum-Implementierung: drei Zeiger (links / rechts / übergeordnet) plus ein Farbbit, das aufgrund der Ausrichtung ein viertes Wort benötigt. Das sind vier Zeiger plus Daten pro Element.
Yakov Galka

126

Wenn Sie die Geschwindigkeit Ihrer std::mapund der std::unordered_mapImplementierungen vergleichen möchten , können Sie das Sparsehash- Projekt von Google verwenden , das über ein time_hash_map-Programm verfügt, um die Zeit zu bestimmen. Zum Beispiel mit gcc 4.4.2 auf einem x86_64-Linux-System

$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow              126.1 ns  (27427396 hashes, 40000000 copies)  290.9 MB
map_predict/grow       67.4 ns  (10000000 hashes, 40000000 copies)  232.8 MB
map_replace            22.3 ns  (37427396 hashes, 40000000 copies)
map_fetch              16.3 ns  (37427396 hashes, 40000000 copies)
map_fetch_empty         9.8 ns  (10000000 hashes,        0 copies)
map_remove             49.1 ns  (37427396 hashes, 40000000 copies)
map_toggle             86.1 ns  (20000000 hashes, 40000000 copies)

STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow              225.3 ns  (       0 hashes, 20000000 copies)  462.4 MB
map_predict/grow      225.1 ns  (       0 hashes, 20000000 copies)  462.6 MB
map_replace           151.2 ns  (       0 hashes, 20000000 copies)
map_fetch             156.0 ns  (       0 hashes, 20000000 copies)
map_fetch_empty         1.4 ns  (       0 hashes,        0 copies)
map_remove            141.0 ns  (       0 hashes, 20000000 copies)
map_toggle             67.3 ns  (       0 hashes, 20000000 copies)

2
Es sieht so aus, als ob eine ungeordnete Karte die Karte bei den meisten Operationen übertrifft.
Michael IV

7
Sparsehash gibt es nicht mehr. es wurde gelöscht oder entfernt.
User9102d82

1
@ User9102d82 Ich habe die Frage so bearbeitet, dass sie auf einen Waybackmachine-Link verweist .
andreee

Nur um sicherzustellen, dass andere neben der Zeit auch die anderen Zahlen bemerken: Diese Tests wurden mit 4-Byte-Objekten / Datenstrukturen, auch bekannt als int, durchgeführt. Wenn Sie etwas speichern, das schwereres Hashing erfordert oder größer ist (wodurch die Kopiervorgänge schwerer werden), hat die Standardkarte möglicherweise schnell einen Vorteil!
AlexGeorg

82

Ich würde ungefähr den gleichen Punkt wiederholen, den GMan gemacht hat: Je nach Art der Verwendung std::mapkann (und ist) dies schneller als std::tr1::unordered_map(unter Verwendung der in VS 2008 SP1 enthaltenen Implementierung).

Es gibt einige komplizierende Faktoren, die zu beachten sind. Zum Beispiel std::mapvergleichen Sie in Schlüssel, was bedeutet, dass Sie immer nur genug vom Anfang eines Schlüssels betrachten, um zwischen dem rechten und dem linken Unterzweig des Baums zu unterscheiden. Nach meiner Erfahrung ist es fast das einzige Mal, dass Sie einen ganzen Schlüssel betrachten, wenn Sie so etwas wie int verwenden, das Sie in einer einzigen Anweisung vergleichen können. Bei einem typischeren Schlüsseltyp wie std :: string vergleichen Sie häufig nur wenige Zeichen oder so.

Eine anständige Hash-Funktion hingegen betrachtet immer den gesamten Schlüssel. IOW, selbst wenn die Tabellensuche eine konstante Komplexität aufweist, weist der Hash selbst eine ungefähr lineare Komplexität auf (allerdings auf der Länge des Schlüssels, nicht auf der Anzahl der Elemente). Mit langen Zeichenfolgen als Schlüssel kann std::mapeine Suche beendet werden, bevor eine Suche unordered_mapüberhaupt gestartet wird .

Zweitens, obwohl es verschiedene Methoden zum Ändern der Größe von Hash-Tabellen gibt, sind die meisten davon ziemlich langsam - bis zu dem Punkt, dass std :: map oft schneller ist als , wenn Suchvorgänge nicht wesentlich häufiger sind als Einfügungen und Löschungen std::unordered_map.

Natürlich können Sie, wie ich im Kommentar zu Ihrer vorherigen Frage erwähnt habe, auch eine Baumtabelle verwenden. Dies hat sowohl Vor- als auch Nachteile. Einerseits beschränkt es den schlimmsten Fall auf den eines Baumes. Es ermöglicht auch ein schnelles Einfügen und Löschen, da ich (zumindest wenn ich es getan habe) eine Tabelle mit fester Größe verwendet habe. Die Beseitigung aller Tabelle Redimensionierung können Sie Ihre Hash - Tabelle viel einfacher und in der Regel schneller halten.

Ein weiterer Punkt: Die Anforderungen für Hashing und baumbasierte Karten sind unterschiedlich. Das Hashing erfordert offensichtlich eine Hash-Funktion und einen Gleichheitsvergleich, wobei geordnete Karten einen weniger als Vergleich erfordern. Natürlich erfordert der von mir erwähnte Hybrid beides. Für den allgemeinen Fall, dass eine Zeichenfolge als Schlüssel verwendet wird, ist dies natürlich kein wirkliches Problem, aber einige Arten von Schlüsseln passen besser zur Reihenfolge als zum Hashing (oder umgekehrt).


2
Die Größenänderung von Hashs kann durch dynamic hashingTechniken gedämpft werden, die darin bestehen, eine Übergangszeit zu haben, in der Sie jedes Mal, wenn Sie ein Element einfügen, auch kandere Elemente erneut aufbereiten . Natürlich bedeutet dies, dass Sie während des Übergangs 2 verschiedene Tabellen durchsuchen müssen ...
Matthieu M.

2
"Mit langen Zeichenfolgen als Schlüssel kann eine std :: map eine Suche beenden, bevor eine ungeordnete Karte überhaupt ihre Suche startet." - wenn der Schlüssel nicht in der Sammlung vorhanden ist. Wenn es vorhanden ist, muss natürlich die gesamte Länge verglichen werden, um die Übereinstimmung zu bestätigen. Sie müssen jedoch ebenfalls unordered_mapeine Hash-Übereinstimmung mit einem vollständigen Vergleich bestätigen, damit alles davon abhängt, welchen Teilen des Suchprozesses Sie gegenüberstehen.
Steve Jessop

2
Normalerweise können Sie die Hash-Funktion basierend auf der Kenntnis der Daten ersetzen. Wenn zum Beispiel Ihre langen Zeichenfolgen in den letzten 20 Bytes stärker variieren als in den ersten 100,
hashen Sie

56

Ich war fasziniert von der Antwort von @Jerry Coffin, die darauf hinwies, dass die geordnete Karte bei langen Zeichenfolgen Leistungssteigerungen aufweisen würde. Nach einigen Experimenten (die von Pastebin heruntergeladen werden können ) habe ich festgestellt, dass dies nur für Sammlungen zu gelten scheint Bei zufälligen Zeichenfolgen bricht diese Regel zusammen, wenn die Karte mit einem sortierten Wörterbuch initialisiert wird (das Wörter mit erheblichen Präfixüberlappungen enthält), vermutlich aufgrund der erhöhten Baumtiefe, die zum Abrufen des Werts erforderlich ist. Die Ergebnisse sind unten gezeigt, die erste Zahlenspalte ist die Einfügezeit, die zweite ist die Abrufzeit.

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
 ** Integer Keys ** 
 unordered:      137      15
   ordered:      168      81
 ** Random String Keys ** 
 unordered:       55      50
   ordered:       33      31
 ** Real Words Keys ** 
 unordered:      278      76
   ordered:      516     298

2
Danke für den Test. Um sicherzustellen, dass wir kein Rauschen messen, habe ich es so geändert, dass es jede Operation mehrmals ausführt (und den Zähler anstelle von 1 in die Karte eingefügt). Ich habe es über eine andere Anzahl von Schlüsseln (von 2 bis 1000) und bis zu ~ 100 Schlüsseln in der Karte ausgeführt, die std::mapnormalerweise besser abschneiden std::unordered_map, insbesondere bei Ganzzahlschlüsseln, aber bei ~ 100 Schlüsseln scheint es, als ob es seinen Rand verliert und std::unordered_mapanfängt zu gewinnen. Das Einfügen einer bereits geordneten Sequenz in eine std::mapist sehr schlecht. Sie erhalten das Worst-Case-Szenario (O (N)).
Andreas Magnusson

30

Ich möchte nur darauf hinweisen, dass ... es viele Arten von unordered_maps gibt.

Schlagen Sie den Wikipedia-Artikel auf der Hash-Karte nach. Je nachdem, welche Implementierung verwendet wurde, können die Merkmale hinsichtlich Nachschlagen, Einfügen und Löschen erheblich variieren.

Und das ist es, was mich am meisten beunruhigt, wenn ich die unordered_mapSTL hinzufüge: Sie müssen eine bestimmte Implementierung auswählen, da ich bezweifle, dass sie die PolicyStraße hinuntergehen werden , und so werden wir bei einer Implementierung für den durchschnittlichen Gebrauch und nichts für bleiben die anderen Fälle ...

Zum Beispiel haben einige Hash-Maps eine lineare Aufbereitung, wobei anstelle einer erneuten Aufbereitung der gesamten Hash-Map bei jeder Einfügung ein Teil erneut aufbereitet wird, was zur Amortisation der Kosten beiträgt.

Ein weiteres Beispiel: Einige Hash-Maps verwenden eine einfache Liste von Knoten für einen Bucket, andere verwenden eine Map, andere verwenden keine Knoten, sondern suchen den nächsten Steckplatz, und einige verwenden eine Liste von Knoten, ordnen sie jedoch so an, dass das Element, auf das zuletzt zugegriffen wird ist vorne (wie ein Caching-Ding).

Im Moment bevorzuge ich eher das std::mapoder vielleicht ein loki::AssocVector(für eingefrorene Datensätze).

Verstehen Sie mich nicht falsch, ich würde das gerne verwenden, std::unordered_mapund ich kann es in Zukunft tun , aber es ist schwierig, der Portabilität eines solchen Containers zu "vertrauen", wenn Sie über alle Implementierungsmöglichkeiten und die verschiedenen daraus resultierenden Leistungen nachdenken von diesem.


17
+1: gültiger Punkt - das Leben war einfacher, als ich meine eigene Implementierung verwendete - zumindest wusste ich, wo es saugte:>
Kornel Kisielewicz

25

Wesentliche Unterschiede, die hier nicht ausreichend erwähnt wurden:

  • mapHält Iteratoren für alle Elemente stabil. In C ++ 17 können Sie sogar Elemente von einem mapzum anderen verschieben, ohne die Iteratoren für sie ungültig zu machen (und bei ordnungsgemäßer Implementierung ohne potenzielle Zuordnung).
  • map Die Zeitabläufe für einzelne Vorgänge sind in der Regel konsistenter, da sie niemals große Zuordnungen erfordern.
  • unordered_mapDie Verwendung std::hashwie in libstdc ++ implementiert ist für DoS anfällig, wenn sie mit nicht vertrauenswürdigen Eingaben gespeist wird (es wird MurmurHash2 mit einem konstanten Startwert verwendet - nicht, dass das Startwert wirklich helfen würde, siehe https://emboss.github.io/blog/2012/12/14/ Breaking-Murmeln-Hash-Flooding-Dos-Reloaded / ).
  • Die Bestellung ermöglicht eine effiziente Bereichssuche, z. B. das Durchlaufen aller Elemente mit der Taste ≥ 42.

14

Hash-Tabellen haben höhere Konstanten als gängige Map-Implementierungen, die für kleine Container von Bedeutung sind. Die maximale Größe beträgt 10, 100 oder vielleicht sogar 1.000 oder mehr? Konstanten sind die gleichen wie immer, aber O (log n) liegt nahe bei O (k). (Denken Sie daran, dass die logarithmische Komplexität immer noch sehr gut ist.)

Was eine gute Hash-Funktion ausmacht, hängt von den Eigenschaften Ihrer Daten ab. Wenn ich also nicht vorhabe, eine benutzerdefinierte Hash-Funktion zu betrachten (aber meine Meinung später sicherlich ändern kann, und zwar leicht, da ich fast alles tippe), und obwohl die Standardeinstellungen so gewählt sind, dass sie für viele Datenquellen eine anständige Leistung erbringen, finde ich die geordnete Die Art der Karte ist anfangs eine ausreichende Hilfe, sodass ich in diesem Fall immer noch standardmäßig eine Karte anstelle einer Hash-Tabelle verwende.

Außerdem müssen Sie auf diese Weise nicht einmal daran denken, eine Hash-Funktion für andere (normalerweise UDT) Typen zu schreiben, und einfach op <schreiben (was Sie sowieso wollen).


@ Roger, kennen Sie die ungefähre Anzahl von Elementen, bei denen unordered_map die beste Karte darstellt? Ich werde wahrscheinlich trotzdem einen Test dafür schreiben ... (+1)
Kornel Kisielewicz

1
@Kornel: Es braucht nicht sehr viele; Meine Tests waren mit ungefähr 10.000 Elementen. Wenn wir ein wirklich genaues Diagramm wünschen , können Sie sich eine Implementierung von mapund eine unordered_mapmit einer bestimmten Plattform und einer bestimmten Cache-Größe ansehen und eine komplexe Analyse durchführen. : P
GManNickG

Abhängig von Implementierungsdetails, Optimierungsparametern zur Kompilierungszeit (einfach zu unterstützen, wenn Sie Ihre eigene Implementierung schreiben) und sogar dem spezifischen Computer, der für die Tests verwendet wird. Genau wie bei den anderen Containern legt der Ausschuss nur die allgemeinen Anforderungen fest.

13

Gründe wurden in anderen Antworten angegeben; hier ist noch einer.

std :: map-Operationen (ausgeglichener Binärbaum) werden mit O (log n) und Worst-Case-O (log n) abgeschrieben. std :: unordered_map (Hash-Tabelle) Operationen werden amortisiert O (1) und Worst-Case O (n).

Wie sich dies in der Praxis auswirkt, ist, dass die Hash-Tabelle gelegentlich mit einer O (n) -Operation "Schluckauf" hat, was Ihre Anwendung möglicherweise tolerieren kann oder nicht. Wenn es nicht toleriert werden kann, bevorzugen Sie std :: map gegenüber std :: unordered_map.


12

Zusammenfassung

Vorausgesetzt, die Bestellung ist nicht wichtig:

  • Wenn Sie einmal eine große Tabelle erstellen und viele Abfragen durchführen möchten, verwenden Sie std::unordered_map
  • Wenn Sie eine kleine Tabelle erstellen möchten (möglicherweise weniger als 100 Elemente) und viele Abfragen durchführen, verwenden Sie std::map. Dies liegt daran, dass darauf gelesen wird O(log n).
  • Wenn Sie die Tabelle häufig wechseln , std::map ist dies möglicherweise eine gute Option.
  • Wenn Sie Zweifel haben, verwenden Sie einfach std::unordered_map.

Historischer Zusammenhang

In den meisten Sprachen sind ungeordnete Karten (auch als Hash-basierte Wörterbücher bezeichnet) die Standardkarte. In C ++ erhalten Sie jedoch eine geordnete Karte als Standardkarte. Wie ist das passiert? Einige Leute gehen fälschlicherweise davon aus, dass das C ++ - Komitee diese Entscheidung in ihrer einzigartigen Weisheit getroffen hat, aber die Wahrheit ist leider hässlicher.

Es wird allgemein angenommen, dass C ++ standardmäßig eine geordnete Karte hat, da es nicht zu viele Parameter gibt, wie sie implementiert werden können. Auf der anderen Seite gibt es bei Hash-basierten Implementierungen jede Menge zu besprechen. Um Blockaden bei der Standardisierung zu vermeiden, kamen sie nur mit der geordneten Karte zurecht . Um 2005 hatten viele Sprachen bereits gute Implementierungen der Hash-basierten Implementierung, so dass es für das Komitee einfacher war, neue zu akzeptieren std::unordered_map. In einer perfekten Welt std::mapwäre ungeordnet gewesen und wir hätten std::ordered_mapals separaten Typ.

Performance

Die folgenden zwei Grafiken sollten für sich selbst sprechen ( Quelle ):

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein


Interessante Daten; Wie viele Plattformen haben Sie in Ihre Tests einbezogen?
Toby Speight

1
Warum sollte ich std :: map für kleine Tabellen verwenden, wenn ich viele Abfragen durchführe, da std :: unordered_map gemäß den 2 Bildern, die Sie hier gepostet haben, immer eine bessere Leistung als std :: map erbringt?
Ricky

Die Grafik zeigt die Leistung für 0,13 Mio. oder mehr Elemente. Wenn Sie kleine (möglicherweise <100) Elemente haben, wird O (log n) möglicherweise kleiner als eine ungeordnete Karte.
Shital Shah

10

Ich habe kürzlich einen Test durchgeführt, bei dem 50000 zusammengeführt und sortiert werden. Das heißt, wenn die Zeichenfolgenschlüssel identisch sind, führen Sie die Bytezeichenfolge zusammen. Und die endgültige Ausgabe sollte sortiert werden. Dies beinhaltet also eine Suche nach jeder Einfügung.

Für die mapImplementierung dauert es 200 ms, um den Job zu beenden. Für das unordered_map+ mapdauert das unordered_mapEinfügen 70 ms und das Einfügen 80 ms map. Die Hybridimplementierung ist also 50 ms schneller.

Wir sollten zweimal überlegen, bevor wir das verwenden map. Wenn Sie nur die Daten im Endergebnis Ihres Programms sortieren müssen, ist eine Hybridlösung möglicherweise besser.


0

Kleine Ergänzung zu allen oben genannten:

Besser verwenden map, wenn Sie Elemente nach Bereich abrufen müssen, da sie sortiert sind und Sie sie einfach von einer Grenze zur anderen durchlaufen können.


-1

Von: http://www.cplusplus.com/reference/map/map/

"Intern werden die Elemente in einer Karte immer nach ihrem Schlüssel sortiert, und zwar nach einem bestimmten strengen Kriterium für schwache Ordnungen, das durch das interne Vergleichsobjekt (vom Typ Vergleichen) angegeben wird.

Map-Container sind im Allgemeinen langsamer als unordered_map-Container, um über ihren Schlüssel auf einzelne Elemente zuzugreifen. Sie ermöglichen jedoch die direkte Iteration von Teilmengen basierend auf ihrer Reihenfolge. "

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.