it-swarm.asia

هل هناك أي ميزة لاستخدام الخريطة على unordered_map في حالة المفاتيح التافهة؟

الحديث الذي أجري مؤخراً حول unordered_map في C++ جعلني أدرك أنه يجب علي استخدام unordered_map في معظم الحالات التي كنت أستخدم فيها map من قبل ، نظرًا لكفاءة البحث ( الإطفاء O(1) vs . O (log n) ). في معظم الأحيان أستخدم الخريطة التي أستخدمها إما int أو std::strings كمفاتيح ، وبالتالي لم أواجه أية مشاكل في تعريف وظيفة التجزئة. كلما فكرت في الأمر ، كلما أدركت أنه لا يمكنني العثور على أي سبب لاستخدام std::map في حالة وجود أنواع بسيطة عبر unordered_map - ألقيت نظرة على الواجهات ولم أجد أي أهمية الاختلافات التي من شأنها أن تؤثر على بلدي رمز.

ومن هنا السؤال - هل هناك أي سبب حقيقي لاستخدام std::map على unordered map في حالة الأنواع البسيطة مثل int و std::string؟

أنا أسأل من وجهة نظر برمجة صارمة - أعرف أنها ليست معيارًا كاملاً ، وقد تشكل مشكلات في النقل.

أتوقع أيضًا أن تكون إحدى الإجابات الصحيحة "إنها أكثر فاعلية لمجموعات أصغر من البيانات" بسبب مقدار حمل أصغر (هل هذا صحيح؟) - ومن ثم أود أن أقصر السؤال على الحالات التي يكون فيها مقدار المفاتيح غير تافه (> 1 024).

تحرير: duh ، لقد نسيت ما هو واضح (شكرا GMan!) - نعم ، يتم ترتيب الخريطة بالطبع - أعرف ذلك ، وأبحث عن أسباب أخرى.

328
Kornel Kisielewicz

لا تنسَ الاحتفاظ بـ map لعناصرها. إذا لم تستطع التخلي عن ذلك ، فمن الواضح أنك لا تستطيع استخدام unordered_map.

شيء آخر يجب أخذه في الاعتبار هو أن unordered_map تستخدم عمومًا ذاكرة أكثر. يحتوي map على عدد قليل من مؤشرات تدبير شؤون المنزل ثم الذاكرة لكل كائن. على العكس من ذلك ، يحتوي unordered_map على صفيف كبير (يمكن أن تصبح هذه كبيرة جدًا في بعض التطبيقات) ثم ذاكرة إضافية لكل كائن. إذا كنت بحاجة إلى أن تكون مدركًا للذاكرة ، فيجب أن يكون map أفضل ، لأنه يفتقر إلى الصفيف الكبير.

لذلك ، إذا كنت بحاجة إلى البحث عن محض ، فأنا أقول أن unordered_map هو السبيل للذهاب. ولكن هناك دائمًا مقايضات ، وإذا كنت لا تستطيع تحملها ، فلا يمكنك استخدامها.

فقط من التجربة الشخصية ، وجدت تحسناً هائلاً في الأداء (يتم قياسه ، بالطبع) عند استخدام unordered_map بدلاً من map في جدول بحث عن كيان رئيسي.

من ناحية أخرى ، وجدت أنه كان أبطأ بكثير في إدخال وإزالة العناصر بشكل متكرر. يعد هذا الأمر رائعًا بالنسبة لمجموعة من العناصر الثابتة نسبيًا ، ولكن إذا كنت تفعل الكثير من عمليات الإدراج والحذف ، فيبدو أن علامة التجزئة + الإضافة تضيف ما يصل. (لاحظ أن هذا كان على العديد من التكرارات.)

360
GManNickG

إذا كنت ترغب في مقارنة سرعة تطبيقي std::map و std::unordered_map ، فيمكنك استخدام sparsehash مشروع Google الذي يحتوي على برنامج time_hash_map لضبطهما. على سبيل المثال ، مع gcc 4.4.2 على نظام Linux x86_64

$ ./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)
111
Blair Zajac

أود أن أكرر نفس النقطة التي أوضحها GMan: وفقًا لنوع الاستخدام ، يمكن أن يكون std::map أسرع (وغالبًا ما يكون) من std::tr1::unordered_map (باستخدام التطبيق المضمن في VS 2008 SP1).

هناك بعض العوامل المعقدة التي يجب وضعها في الاعتبار. على سبيل المثال ، في std::map ، تقوم بمقارنة المفاتيح ، مما يعني أنك لم تنظر أبدًا إلى ما يكفي من بداية المفتاح للتمييز بين الفرعين الفرعيين الأيمن والأيسر للشجرة. في تجربتي ، المرة الوحيدة التي تنظر فيها إلى المفتاح بالكامل هي إذا كنت تستخدم شيئًا يشبه int يمكنك مقارنته في تعليمة واحدة. مع نوع مفتاح أكثر نموذجية مثل std :: string ، يمكنك في كثير من الأحيان مقارنة بضعة أحرف فقط أو نحو ذلك.

وعلى النقيض من ذلك ، فإن دالة التجزئة اللائقة تنظر دائمًا إلى كامل مفتاح. IOW ، حتى لو كان البحث عن جدول معقدًا بشكل ثابت ، فإن التجزئة نفسها لها تعقيد خطي تقريبًا (على الرغم من طول المفتاح وليس عدد العناصر). مع وجود سلاسل طويلة كمفاتيح ، قد ينهي std::map عملية بحث قبل أن يكون unordered_map حتى يبدأ بحثه.

ثانياً ، على الرغم من أن هناك عدة طرق لتغيير حجم جداول التجزئة ، إلا أن معظمها بطيء جدًا - لدرجة أنه ما لم تكن عمليات البحث إلى حد كبير أكثر تكرارًا من عمليات الإدراج والحذف ، فإن std :: map غالباً ما تكون أسرع من std::unordered_map.

بالطبع ، كما ذكرت في التعليق على سؤالك السابق ، يمكنك أيضًا استخدام جدول الأشجار. وهذا له مزايا وعيوب. من ناحية ، فإنه يحد الحالة الأسوأ لحالة الشجرة. كما أنه يتيح الإدراج والحذف بسرعة ، لأنه (على الأقل عندما قمت بذلك) لقد استخدمت حجمًا ثابتًا من الجدول. يتيح لك إزالة الكل تغيير حجم الجدول الاحتفاظ بجدول التجزئة الخاص بك بشكل أبسط وأسرع عادةً.

نقطة أخرى: تختلف متطلبات التجزئة والخرائط القائمة على الأشجار. من الواضح أن التجزئة تتطلب وظيفة تجزئة ، ومقارنة المساواة ، حيث تتطلب الخرائط المطلوبة مقارنة أقل من المقارنة. بالطبع الهجين الذي ذكرته يتطلب كليهما. بالطبع ، بالنسبة للحالة الشائعة المتمثلة في استخدام سلسلة كمفتاح ، فإن هذه ليست مشكلة حقًا ، ولكن بعض أنواع المفاتيح تناسب الترتيب أفضل من التجزئة (أو العكس).

75
Jerry Coffin

لقد دهشت من إجابة @ Jerry Coffin ، التي أشارت إلى أن الخريطة المطلوبة تعرض زيادات في الأداء على الأوتار الطويلة ، بعد بعض التجارب (التي يمكن تنزيلها من Pastebin ) ، لقد وجدت أن هذا يبدو فقط ينطبق على مجموعات السلاسل العشوائية ، عندما تتم تهيئة الخريطة بقاموس مصنَّف (يحتوي على كلمات تحتوي على كميات كبيرة من بادئة التداخل) ، تنهار هذه القاعدة ، على الأرجح بسبب زيادة عمق الشجرة الضروري لاسترداد القيمة. النتائج موضحة أدناه ، عمود الرقم الأول هو وقت الإدراج ، الثاني هو وقت الجلب.

g++ -g -O3 --std=c++0x   -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
[email protected]: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
50
Gearoid Murphy

أود فقط أن أشير إلى أن ... هناك أنواع كثيرة من unordered_maps.

ابحث عن مقالة ويكيبيديا على خريطة التجزئة. اعتمادًا على التطبيق المستخدم ، قد تختلف الخصائص من حيث البحث والإدراج والحذف بشكل كبير.

وهذا هو أكثر ما يقلقني بإضافة unordered_map إلى STL: سيتعين عليهم اختيار تطبيق معين لأنني أشك في أنهم سوف يسيروا في الطريق Policy ، وبالتالي سوف نلتزم بتنفيذ متوسط ​​الاستخدام و لا شيء للحالات الأخرى ...

على سبيل المثال ، تحتوي بعض خرائط التجزئة على إعادة صياغة خطية ، حيث بدلاً من إعادة صياغة خريطة التجزئة بأكملها دفعة واحدة ، تتم إعادة صياغة جزء عند كل إدخال ، مما يساعد على استهلاك التكلفة.

مثال آخر: تستخدم بعض خرائط التجزئة قائمة بسيطة من العقد لدلو ، والبعض الآخر يستخدم خريطة ، والبعض الآخر لا يستخدم العقد ولكنهم يعثرون على أقرب فتحة ، وآخرهم سيستخدم قائمة العقد ولكن يعيد ترتيبها حتى آخر عنصر تم الوصول إليه في المقدمة (مثل شيء التخزين المؤقت).

لذا ، أميل في الوقت الحالي إلى تفضيل std::map أو ربما loki::AssocVector (لمجموعات البيانات المجمدة).

لا تفهموني بشكل خاطئ ، أرغب في استخدام std::unordered_map وقد أستخدمه في المستقبل ، لكن من الصعب "الوثوق" في قابلية نقل هذه الحاوية عندما تفكر في جميع طرق تنفيذها والأداء المختلف الذي نتيجة هذا.

29
Matthieu M.

اختلافات كبيرة لم يتم ذكرها بشكل كافٍ هنا:

  • map يحافظ على التكرارات لجميع العناصر مستقرة ، في C++ 17 ، يمكنك حتى نقل العناصر من map إلى الآخر دون إبطال التكرار عليها (وإذا تم تنفيذها بشكل صحيح دون أي تخصيص محتمل).
  • توقيت map للعمليات الفردية عادة ما يكون أكثر تناسقًا لأنها لا تحتاج أبدًا إلى تخصيصات كبيرة.
  • unordered_map باستخدام std::hash كما هو مطبق في libstdc ++ يكون عرضة لـ DoS إذا تم تغذيته بإدخال غير موثوق به (يستخدم MurmurHash2 مع بذرة ثابتة - وليس أن البذر سيساعد حقًا ، انظر https://emboss.github.io/blog/2012/ 12/14/كسر-نفخة تجزئة-دوس-إعادة تحميل/ ).
  • يُمكن الطلب من البحث الفعال في النطاق ، على سبيل المثال التكرار على جميع العناصر مع مفتاح ≥ 42.
16
user1531083

تحتوي جداول التجزئة على ثوابت أعلى من تطبيقات الخرائط الشائعة ، والتي تصبح مهمة للحاويات الصغيرة. الحجم الأقصى هو 10 ، 100 ، أو ربما حتى 1000 أو أكثر؟ الثوابت هي نفسها كما كانت دائمًا ، لكن O (log n) قريب من O (k). (تذكر أن التعقيد اللوغاريتمي لا يزال حقًا جيدًا)

ما يجعل وظيفة تجزئة جيدة تعتمد على خصائص البيانات الخاصة بك ؛ لذلك إذا كنت لا أخطط للنظر في وظيفة تجزئة مخصصة (ولكن يمكنني بالتأكيد تغيير رأيي لاحقًا ، وبسهولة منذ أن قمت بكتابة لعنة بالقرب من كل شيء) وعلى الرغم من اختيار الإعدادات الافتراضية لأداء لائق للعديد من مصادر البيانات ، فإنني أجد الأمر يجب أن تكون طبيعة الخريطة كافية للمساعدة في البداية ، ولا أزال أتقيد في التعيين بدلاً من جدول التجزئة في هذه الحالة.

بالإضافة إلى ذلك ، لن تضطر إلى التفكير في كتابة دالة تجزئة لأنواع أخرى (عادةً UDT) ، ثم اكتب op ((التي تريدها على أي حال).

14
Roger Pate

تم ذكر الأسباب في إجابات أخرى ؛ هنا شيء آخر.

sTD :: map (شجرة ثنائية متوازنة) يتم إطفاء عمليات O (log n) وأسوأ حالة O (log n). يتم إطفاء عمليات std :: unordered_map (جدول التجزئة) O(1) وأسوأ حالة O (n).

كيف يتم تنفيذ ذلك في الممارسة العملية هو أن جدول التجزئة "الفواق" كل مرة واحدة مع عملية O(n) ، والتي قد تكون أو لا تكون شيئًا يمكن أن يحتمل تطبيقك. إذا لم يكن بإمكانها تحمل ذلك ، فستفضل std :: map على std :: unordered_map.

10
Don Hatch

لقد أجريت اختبارًا مؤخرًا مما يجعل 50000 دمجًا وفرزًا. هذا يعني أنه إذا كانت مفاتيح السلسلة متماثلة ، فادمج سلسلة البايت. وينبغي فرز الناتج النهائي. لذلك هذا يشمل البحث عن كل الإدراج.

لتطبيق map ، يستغرق الأمر 200 مللي ثانية لإنهاء المهمة. بالنسبة لـ unordered_map + map ، يستغرق الأمر 70 مللي ثانية للإدخال unordered_map و 80 ms للإدراج map. وبالتالي فإن التنفيذ المختلط أسرع بـ 50 مللي ثانية.

يجب أن نفكر مرتين قبل استخدام map. إذا كنت بحاجة فقط إلى فرز البيانات في النتيجة النهائية للبرنامج ، فقد يكون الحل المختلط أفضل.

10
wendong

ملخص

بافتراض أن الطلب ليس مهمًا:

  • إذا كنت ستقوم بإنشاء جدول كبير مرة واحدة وتقوم بالكثير من الاستعلامات ، فاستخدم std::unordered_map
  • إذا كنت بصدد بناء طاولة صغيرة (قد تكون أقل من 100 عنصر) وتقوم بالكثير من الاستعلامات ، فاستخدم std::map. هذا لأن القراءة عليه هي O(log n).
  • إذا كنت تريد تغيير الجدول كثيرًا ، فسيكون قد يكونstd::map خيارًا جيدًا.
  • إذا كنت في شك ، فما عليك سوى استخدام std::unordered_map.

السياق التاريخي

في معظم اللغات ، تعتبر الخريطة غير المرتبة (التي تعرف أيضًا باسم القواميس القائمة على أساس التجزئة) هي الخريطة الافتراضية ، لكن في C++ ، تحصل على خريطة مرتبة كخريطة افتراضية. كيف حدث هذا؟ يفترض بعض الناس خطأً أن لجنة C++ اتخذت هذا القرار بحكمتهم الفريدة ولكن الحقيقة هي أبشع من ذلك للأسف.

على نطاق واسع يعتقد أن C++ انتهى بها الأمر إلى خريطة مرتبة كقيمة افتراضية لأنه لا يوجد الكثير من المعلمات حول كيفية تنفيذها. من ناحية أخرى ، فإن التطبيقات القائمة على التجزئة لديها الكثير من الأشياء التي يمكن الحديث عنها. لذلك لتجنب تعثرات الشوائب في التقييس ، فقط حصلت على طول مع خريطة مرتبة. في حوالي عام 2005 ، كان لدى العديد من اللغات بالفعل تطبيقات جيدة للتطبيق المستند إلى التجزئة ، وبالتالي كان من الأسهل على اللجنة قبول std::unordered_map الجديد. في عالم مثالي ، لم يتم إلغاء ترتيب std::map وسيكون لدينا std::ordered_map كنوع منفصل.

الأداء

أدناه يجب أن تتحدث الرسوم البيانية عن نفسها ( المصدر ):

enter image description here

enter image description here

3
Shital Shah

إضافة صغيرة إلى كل ما سبق:

استخدم بشكل أفضل map ، عندما تحتاج إلى الحصول على عناصر حسب النطاق ، حيث يتم فرزها ويمكنك تكرارها فقط من حدود إلى أخرى.

0
Denis Sablukov