การใช้งาน gcc std :: unordered_map ช้าหรือไม่ ถ้าเป็นเช่นนั้น - ทำไม?


100

เรากำลังพัฒนาซอฟต์แวร์สำคัญที่มีประสิทธิภาพสูงใน C ++ ที่นั่นเราต้องมีแผนที่แฮชพร้อมกันและนำไปใช้ std::unordered_mapดังนั้นเราจึงเขียนมาตรฐานที่จะคิดออกเท่าไหร่ช้ากว่าแผนที่กัญชาพร้อมกันของเราจะถูกเมื่อเทียบกับ

แต่std::unordered_mapดูเหมือนว่าจะไม่น่าเชื่อช้า ... ดังนั้นนี้เป็นของเราไมโครมาตรฐาน (สำหรับแผนที่พร้อมกันเรากลับกลายเป็นหัวข้อใหม่เพื่อให้แน่ใจว่าการล็อคไม่ได้รับการปรับให้เหมาะสมออกไปและทราบว่าฉันไม่เคย Inser 0 เพราะผมยังมาตรฐานด้วยgoogle::dense_hash_map, ซึ่งต้องการค่า null):

boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> vec(SIZE);
for (int i = 0; i < SIZE; ++i) {
    uint64_t val = 0;
    while (val == 0) {
        val = dist(rng);
    }
    vec[i] = val;
}
std::unordered_map<int, long double> map;
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < SIZE; ++i) {
    map[vec[i]] = 0.0;
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "inserts: " << elapsed.count() << std::endl;
std::random_shuffle(vec.begin(), vec.end());
begin = std::chrono::high_resolution_clock::now();
long double val;
for (int i = 0; i < SIZE; ++i) {
    val = map[vec[i]];
}
end = std::chrono::high_resolution_clock::now();
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "get: " << elapsed.count() << std::endl;

(แก้ไข: ซอร์สโค้ดทั้งหมดสามารถพบได้ที่นี่: http://pastebin.com/vPqf7eya )

ผลลัพธ์สำหรับstd::unordered_mapคือ:

inserts: 35126
get    : 2959

สำหรับgoogle::dense_map:

inserts: 3653
get    : 816

สำหรับแผนที่พร้อมกันที่สำรองไว้ด้วยมือของเรา (ซึ่งทำการล็อคแม้ว่าเกณฑ์มาตรฐานจะเป็นเธรดเดียว - แต่อยู่ในเธรดการวางไข่ที่แยกจากกัน):

inserts: 5213
get    : 2594

หากฉันรวบรวมโปรแกรมเปรียบเทียบโดยไม่รองรับ pthread และเรียกใช้ทุกอย่างในเธรดหลักฉันจะได้ผลลัพธ์ต่อไปนี้สำหรับแผนที่พร้อมกันที่สำรองไว้ด้วยมือของเรา:

inserts: 4441
get    : 1180

ฉันรวบรวมด้วยคำสั่งต่อไปนี้:

g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc

ดังนั้นโดยเฉพาะอย่างยิ่งการแทรกบนstd::unordered_mapดูเหมือนจะมีราคาแพงมาก - 35 วินาทีเทียบกับ 3-5 วินาทีสำหรับแผนที่อื่น ๆ นอกจากนี้เวลาในการค้นหาดูเหมือนจะค่อนข้างสูง

คำถามของฉัน: ทำไมถึงเป็นเช่นนี้? ฉันอ่านคำถามอื่นเกี่ยวกับ stackoverflow ที่มีคนถามทำไมถึงstd::tr1::unordered_mapช้ากว่าการใช้งานของเขาเอง มีคำตอบที่ได้รับคะแนนสูงสุดระบุว่าstd::tr1::unordered_mapจำเป็นต้องใช้อินเทอร์เฟซที่ซับซ้อนมากขึ้น แต่ฉันไม่เห็นอาร์กิวเมนต์นี้: เราใช้วิธีการฝากข้อมูลใน concurrent_map ของเราstd::unordered_mapใช้วิธีการฝากข้อมูลด้วย ( google::dense_hash_mapไม่ แต่std::unordered_mapอย่างน้อยควรเร็วกว่าเวอร์ชันที่ปลอดภัยพร้อมกันที่สำรองไว้ในมือของเราหรือไม่) นอกเหนือจากนั้นฉันไม่เห็นอะไรในอินเทอร์เฟซที่บังคับใช้คุณลักษณะที่ทำให้แฮชแมปทำงานได้ไม่ดี ...

ดังนั้นคำถามของฉัน: เป็นเรื่องจริงที่std::unordered_mapดูเหมือนว่าจะช้ามาก? ถ้าไม่: มีอะไรผิดปกติ? ถ้าใช่: อะไรคือสาเหตุของสิ่งนั้น

และคำถามหลักของฉัน: ทำไมการใส่ค่าเข้าไปในstd::unordered_mapราคาแพงมาก (แม้ว่าเราจะจองพื้นที่ไว้เพียงพอในตอนเริ่มต้น แต่ก็ไม่ได้ผลดีกว่ามากนัก - ดังนั้นการเปลี่ยนใหม่จึงดูเหมือนจะไม่ใช่ปัญหา)

แก้ไข:

ก่อนอื่น: ใช่เกณฑ์มาตรฐานที่นำเสนอนั้นไม่มีที่ติ - นี่เป็นเพราะเราเล่นกับมันมากและมันเป็นเพียงการแฮ็ก (เช่นการuint64กระจายเพื่อสร้าง ints ในทางปฏิบัติจะไม่ใช่ความคิดที่ดียกเว้น 0 ในลูป เป็นคนโง่ ฯลฯ ... )

ในขณะที่ความคิดเห็นส่วนใหญ่อธิบายว่าฉันสามารถทำให้ unordered_map เร็วขึ้นได้โดยการจัดสรรพื้นที่ให้เพียงพอ ในแอปพลิเคชันของเราสิ่งนี้เป็นไปไม่ได้: เรากำลังพัฒนาระบบจัดการฐานข้อมูลและต้องการแผนที่แฮชเพื่อจัดเก็บข้อมูลบางส่วนระหว่างการทำธุรกรรม (เช่นการล็อกข้อมูล) ดังนั้นแผนที่นี้สามารถเป็นได้ทุกอย่างตั้งแต่ 1 (ผู้ใช้เพียงแค่แทรกและคอมมิต) ไปจนถึงหลายพันล้านรายการ (หากเกิดการสแกนแบบเต็มตาราง) เป็นไปไม่ได้เลยที่จะจัดสรรพื้นที่ให้เพียงพอที่นี่ (และการจัดสรรจำนวนมากในตอนแรกจะใช้หน่วยความจำมากเกินไป)

นอกจากนี้ฉันขออภัยที่ฉันไม่ได้ระบุคำถามของฉันให้ชัดเจนเพียงพอ: ฉันไม่สนใจที่จะทำให้ unordered_map เร็วขึ้น (การใช้แผนที่แฮชที่หนาแน่นของ googles ทำงานได้ดีสำหรับเรา) ฉันไม่เข้าใจจริงๆว่าความแตกต่างของประสิทธิภาพขนาดใหญ่นี้มาจากไหน . ไม่สามารถเป็นเพียงการจัดสรรล่วงหน้า (แม้จะมีหน่วยความจำที่จัดสรรไว้ล่วงหน้าเพียงพอ แต่แผนที่ที่หนาแน่นจะมีลำดับความสำคัญเร็วกว่า unordered_map แผนที่พร้อมกันที่สำรองไว้ด้วยมือของเราเริ่มต้นด้วยอาร์เรย์ขนาด 64 - ดังนั้นจึงมีขนาดเล็กกว่า unordered_map)

แล้วสาเหตุที่ทำให้ผลงานแย่ขนาดนี้std::unordered_mapคืออะไร? หรือถามแตกต่างกัน: เราสามารถเขียนการใช้งานstd::unordered_mapอินเทอร์เฟซที่เป็นไปตามมาตรฐานและ (เกือบ) เร็วเท่าแผนที่แฮชที่หนาแน่นของ googles ได้หรือไม่? หรือมีบางอย่างในมาตรฐานที่บังคับให้ผู้ปฏิบัติงานเลือกวิธีที่ไม่มีประสิทธิภาพในการนำไปใช้?

แก้ไข 2:

โดยการทำโปรไฟล์ฉันเห็นว่ามีการใช้เวลาเป็นจำนวนมากสำหรับการหารจำนวนเต็ม std::unordered_mapใช้จำนวนเฉพาะสำหรับขนาดอาร์เรย์ในขณะที่การใช้งานอื่น ๆ ใช้พาวเวอร์ของสอง เหตุใดจึงstd::unordered_mapใช้จำนวนเฉพาะ เพื่อให้ทำงานได้ดีขึ้นหากแฮชไม่ดี? สำหรับแฮชที่ดี imho ไม่สร้างความแตกต่าง

แก้ไข 3:

นี่คือตัวเลขสำหรับstd::map:

inserts: 16462
get    : 16978

Sooooooo: ทำไมแทรกในstd::mapเร็วกว่าแทรกในstd::unordered_map... ฉันหมายถึง WAT? std::mapมีตำแหน่งที่แย่กว่า (ต้นไม้เทียบกับอาร์เรย์) จำเป็นต้องทำการจัดสรรเพิ่มเติม (ต่อการแทรกเทียบกับการรีแฮช + บวก ~ 1 สำหรับการชนแต่ละครั้ง) และที่สำคัญที่สุด: มีความซับซ้อนของอัลกอริทึมอื่น (O (บันทึก) เทียบกับ O (1))!


1
ส่วนใหญ่ของภาชนะบรรจุในมาตรฐานเป็นอนุรักษ์นิยมมากกับประมาณการของพวกเขาผมจะต้องดูที่ถังนับคุณกำลังใช้ (ระบุไว้ในคอนสตรัค) SIZEและเพิ่มการคาดคะเนที่ดีกว่าสำหรับคุณ
Ylisar

คุณได้ลองใช้ concurrent_hash_map จาก Intel TBB แล้วหรือยัง? threadingbuildingblocks.org/docs/help/reference/…
MadScientist

1
@MadScientist เราถือว่า TBB ปัญหาคือการออกใบอนุญาต: เป็นโครงการวิจัยและเรายังไม่แน่ใจว่าจะเผยแพร่อย่างไร (โอเพ่นซอร์สส่วนใหญ่แน่นอน - แต่ถ้าเราต้องการอนุญาตให้ใช้ในผลิตภัณฑ์เชิงพาณิชย์ GPLv2 มีข้อ จำกัด เกินไป) นอกจากนี้ยังเป็นที่พึ่งพาอื่น แต่เราอาจจะใช้มันในภายหลังจนถึงตอนนี้เราก็สามารถใช้ชีวิตได้โดยปราศจากมัน
Markus Pilman

1
การใช้งานภายใต้ profiler เช่น valgrind อาจเป็นข้อมูลเชิงลึก
Maxim Egorushkin

1
ตำแหน่งที่ตั้งในตารางแฮชจะดีกว่าพื้นที่ในต้นไม้เล็กน้อยอย่างน้อยที่สุดถ้าฟังก์ชันแฮชเป็นแบบ "สุ่ม" ฟังก์ชันแฮชนั้นช่วยให้คุณแทบไม่สามารถเข้าถึงรายการใกล้เคียงในเวลาใกล้เคียงได้ ข้อดีเพียงอย่างเดียวที่คุณมีคืออาร์เรย์แฮชแท็กเป็นบล็อกที่ต่อเนื่องกัน นั่นอาจเป็นจริงสำหรับต้นไม้ถ้าฮีปไม่ได้แยกส่วนและคุณสร้างต้นไม้ทั้งหมดในคราวเดียว เมื่อขนาดใหญ่กว่าแคชความแตกต่างในพื้นที่จะทำให้ประสิทธิภาพแตกต่างกันเล็กน้อย
Steve314

คำตอบ:


87

ฉันพบสาเหตุ: มันเป็นปัญหาของ gcc-4.7 !!

ด้วยgcc-4.7

inserts: 37728
get    : 2985

ด้วยgcc-4.6

inserts: 2531
get    : 1565

ดังนั้นstd::unordered_mapใน gcc-4.7 จึงเสีย (หรือการติดตั้งของฉันซึ่งเป็นการติดตั้ง gcc-4.7.0 บน Ubuntu - และการติดตั้งอื่นซึ่งเป็น gcc 4.7.1 ในการทดสอบเดเบียน)

ฉันจะส่งรายงานข้อผิดพลาด .. ถึงเวลานั้น: ห้ามใช้std::unordered_mapกับ gcc 4.7!


มีอะไรในเดลต้าจาก 4.6 ที่จะทำให้เกิดสิ่งนั้นหรือไม่?
Mark Canlas

30
มีรายงานอยู่แล้วในรายชื่อผู้รับจดหมาย การอภิปรายดูเหมือนจะชี้ไปที่ "การแก้ไข" ในการmax_load_factorจัดการซึ่งนำไปสู่ความแตกต่างของประสิทธิภาพ
jxh

จังหวะที่ไม่ดีสำหรับข้อบกพร่องนี้! ฉันได้รับประสิทธิภาพที่แย่มากกับ unordered_map แต่ฉันดีใจที่มีการรายงานและ "แก้ไขแล้ว"
Bo Lu

+1 - BBBBBUG ดูดอะไร .. ฉันสงสัยว่าเกิดอะไรขึ้นกับ gcc-4.8.2
ikh

2
มีการอัปเดตเกี่ยวกับข้อบกพร่องนี้หรือไม่ ยังคงมีอยู่สำหรับ GCC (5+) รุ่นที่ใหม่กว่าหรือไม่
rph

21

ฉันเดาว่าคุณมีขนาดไม่ถูกต้องunordered_mapตามที่ Ylisar แนะนำ เมื่อโซ่ยาวเกินไปการใช้งานunordered_mapg ++ จะเปลี่ยนเป็นตารางแฮชที่ใหญ่ขึ้นโดยอัตโนมัติและนี่จะเป็นการลากประสิทธิภาพอย่างมาก ถ้าผมจำไม่ผิดunordered_mapค่าเริ่มต้น 100(เล็กที่สุดที่สำคัญมีขนาดใหญ่กว่า)

ฉันไม่ได้มีในระบบของฉันดังนั้นฉันหมดเวลาด้วยchronotimes()

template <typename TEST>
void time_test (TEST t, const char *m) {
    struct tms start;
    struct tms finish;
    long ticks_per_second;

    times(&start);
    t();
    times(&finish);
    ticks_per_second = sysconf(_SC_CLK_TCK);
    std::cout << "elapsed: "
              << ((finish.tms_utime - start.tms_utime
                   + finish.tms_stime - start.tms_stime)
                  / (1.0 * ticks_per_second))
              << " " << m << std::endl;
}

ฉันใช้ a SIZEของ10000000และต้องเปลี่ยนสิ่งต่างๆเล็กน้อยสำหรับเวอร์ชันboost. โปรดทราบว่าฉันปรับขนาดตารางแฮชไว้ล่วงหน้าเพื่อให้ตรงกับSIZE/DEPTHที่ซึ่งDEPTHเป็นค่าประมาณของความยาวของห่วงโซ่ถังเนื่องจากการชนกันของแฮช

แก้ไข:ฮาวเวิร์ดชี้ออกมาให้ฉันในความคิดเห็นว่าปัจจัยที่โหลดสูงสุดสำหรับการเป็นunordered_map 1ดังนั้นDEPTHตัวควบคุมจะควบคุมจำนวนครั้งที่โค้ดจะทำการ rehash

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> map(SIZE/DEPTH);

void
test_insert () {
    for (int i = 0; i < SIZE; ++i) {
        map[vec[i]] = 0.0;
    }
}

void
test_get () {
    long double val;
    for (int i = 0; i < SIZE; ++i) {
        val = map[vec[i]];
    }
}

int main () {
    for (int i = 0; i < SIZE; ++i) {
        uint64_t val = 0;
        while (val == 0) {
            val = dist(rng);
        }
        vec[i] = val;
    }
    time_test(test_insert, "inserts");
    std::random_shuffle(vec.begin(), vec.end());
    time_test(test_insert, "get");
}

แก้ไข:

ฉันแก้ไขโค้ดเพื่อให้สามารถเปลี่ยนDEPTHได้ง่ายขึ้น

#ifndef DEPTH
#define DEPTH 10000000
#endif

ดังนั้นโดยค่าเริ่มต้นจะเลือกขนาดที่แย่ที่สุดสำหรับตารางแฮช

elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000
elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000
elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000
elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000
elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000
elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100
elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10
elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1

ข้อสรุปของฉันคือไม่มีความแตกต่างด้านประสิทธิภาพอย่างมีนัยสำคัญสำหรับขนาดตารางแฮชเริ่มต้นใด ๆ นอกเหนือจากการทำให้เท่ากับจำนวนส่วนแทรกที่ไม่ซ้ำที่คาดไว้ทั้งหมด นอกจากนี้ฉันไม่เห็นลำดับความแตกต่างของขนาดประสิทธิภาพที่คุณสังเกตเห็น


6
std::unordered_mapมีค่าเริ่มต้นโหลดแฟกเตอร์สูงสุด 1 ดังนั้นยกเว้นจำนวนที่เก็บข้อมูลเริ่มต้น DEPTH ของคุณจะถูกละเว้น map.max_load_factor(DEPTH)ถ้าต้องการให้คุณสามารถ
Howard Hinnant

@HowardHinnant: ขอบคุณสำหรับข้อมูลนั้น ดังนั้นจึงDEPTHถูกเพิกเฉย แต่ก็ยังควบคุมความถี่ที่แผนที่จะได้รับการปรับเปลี่ยนเป็นแผนที่ขนาดใหญ่ คำตอบได้รับการอัปเดตแล้วและขอขอบคุณอีกครั้ง
jxh

@ user315052 ใช่ฉันรู้ว่าฉันสามารถทำให้ดีขึ้นได้โดยให้ขนาดที่มีเหตุผลในตอนเริ่มต้น - แต่ฉันไม่สามารถทำเช่นนั้นในซอฟต์แวร์ของเราได้ (เป็นโครงการวิจัย - DBMS - และที่นั่นฉันไม่รู้ว่าฉันจะแทรกมากแค่ไหน - อาจแตกต่างกันระหว่าง 0 ถึง 1 พันล้าน ... ) แต่ถึงแม้จะมีการสั่งล่วงหน้ามันจะช้ากว่าแผนที่ของเราและช้ากว่า googles density_map - ฉันก็ยังสงสัยว่ามันคืออะไรที่สร้างความแตกต่างอย่างมาก
Markus Pilman

@MarkusPilman: ฉันไม่รู้ว่าผลลัพธ์ของฉันเปรียบเทียบกับของคุณอย่างไรเพราะคุณไม่เคยให้ข้อมูลว่าSIZEคุณทำงานด้วยขนาดไหน ฉันสามารถพูดได้ว่าunordered_mapเร็วเป็นสองเท่าเมื่อDEPTHตั้งค่า1และจัดสรรไว้ล่วงหน้า
jxh

1
@MarkusPilman: เวลาของฉันอยู่ในไม่กี่วินาที ฉันคิดว่าเวลาของคุณอยู่ในหน่วยมิลลิวินาที ถ้าการแทรกที่DEPTHตั้งค่า1เป็นใช้เวลาน้อยกว่า3วินาทีลำดับความสำคัญจะช้าลงอย่างไร
jxh

3

ฉันรันโค้ดของคุณโดยใช้คอมพิวเตอร์64 บิต / AMD / 4 คอร์ (2.1GHz)และให้ผลลัพธ์ดังต่อไปนี้:

MinGW-W64 4.9.2:

ใช้std :: unordered_map:

inserts: 9280 
get: 3302

ใช้std :: map:

inserts: 23946
get: 24824

VC 2015 พร้อมแฟล็กการเพิ่มประสิทธิภาพทั้งหมดที่ฉันรู้:

ใช้std :: unordered_map:

inserts: 7289
get: 1908

ใช้std :: map:

inserts: 19222 
get: 19711

ฉันไม่ได้ทดสอบโค้ดโดยใช้ GCC แต่ฉันคิดว่ามันอาจเทียบได้กับประสิทธิภาพของ VC ดังนั้นหากเป็นเช่นนั้นจริง GCC 4.9 std :: unordered_mapก็ยังใช้งานไม่ได้

[แก้ไข]

ใช่ตามที่มีคนกล่าวไว้ในความคิดเห็นไม่มีเหตุผลที่จะคิดว่าประสิทธิภาพของ GCC 4.9.x จะเทียบได้กับประสิทธิภาพของ VC เมื่อฉันมีการเปลี่ยนแปลงฉันจะทดสอบโค้ดบน GCC

คำตอบของฉันคือการสร้างฐานความรู้บางอย่างสำหรับคำตอบอื่น ๆ


"ฉันไม่ได้ทดสอบโค้ดโดยใช้ GCC แต่ฉันคิดว่ามันอาจเทียบได้กับประสิทธิภาพของ VC" การอ้างสิทธิ์ที่ไม่มีมูลความจริงโดยสิ้นเชิงโดยไม่มีการเปรียบเทียบเทียบเคียงกับที่พบในโพสต์ต้นฉบับ "คำตอบ" นี้ไม่ได้ตอบคำถามในแง่ใด ๆ นับประสาอะไรกับการตอบคำถาม "ทำไม"
4ae1e1

2
"ฉันยังไม่ได้ทดสอบโค้ดโดยใช้ GCC" ... เป็นอย่างไรบ้างที่คุณได้รับและใช้งาน MinGW ในขณะที่รู้เรื่องนี้เพียงเล็กน้อย MinGW เป็นพอร์ตการติดตามอย่างใกล้ชิดของ GCC
underscore_d
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.