เหตุใด HashSet <Point> จึงช้ากว่า HashSet <string> มาก


165

ฉันต้องการจัดเก็บตำแหน่งพิกเซลบางแห่งโดยไม่อนุญาตให้ซ้ำซ้อนดังนั้นสิ่งแรกที่ควรคำนึงถึงคือHashSet<Point>หรือคลาสที่คล้ายกัน HashSet<string>อย่างไรก็ตามเรื่องนี้ดูเหมือนจะช้ามากเมื่อเทียบกับสิ่งที่ชอบ

ตัวอย่างเช่นรหัสนี้:

HashSet<Point> points = new HashSet<Point>();
using (Bitmap img = new Bitmap(1000, 1000))
{
    for (int x = 0; x < img.Width; x++)
    {
        for (int y = 0; y < img.Height; y++)
        {
            points.Add(new Point(x, y));
        }
    }
}

ใช้เวลาประมาณ 22.5 วินาที

ในขณะที่รหัสต่อไปนี้(ซึ่งไม่ใช่ตัวเลือกที่ดีสำหรับเหตุผลที่ชัดเจน)ใช้เวลาเพียง 1.6 วินาที:

HashSet<string> points = new HashSet<string>();
using (Bitmap img = new Bitmap(1000, 1000))
{
    for (int x = 0; x < img.Width; x++)
    {
        for (int y = 0; y < img.Height; y++)
        {
            points.Add(x + "," + y);
        }
    }
}

ดังนั้นคำถามของฉันคือ:

  • มีเหตุผลสำหรับสิ่งนั้นหรือไม่? ฉันตรวจสอบคำตอบนี้แต่ 22.5 วินาทีนั้นมากกว่าจำนวนที่แสดงในคำตอบนั้น
  • มีวิธีที่ดีกว่าในการจัดเก็บคะแนนโดยไม่ซ้ำซ้อนหรือไม่?


อะไรคือ "เหตุผลที่ชัดเจน" เหล่านี้ที่ไม่ได้ใช้สตริงที่ต่อกัน? วิธีที่ดีกว่าในการทำคืออะไรถ้าฉันไม่ต้องการใช้ IEqualityComparer ของตัวเอง
Ivan Yurchenko

คำตอบ:


290

มีสองปัญหา perf ที่เกิดจากโครงสร้างจุด บางสิ่งที่คุณเห็นเมื่อคุณเพิ่มConsole.WriteLine(GC.CollectionCount(0));ในรหัสทดสอบ คุณจะเห็นว่าการทดสอบจุดนั้นต้องการคอลเลกชัน ~ 3720 แต่การทดสอบสตริงนั้นต้องการเพียง 18 คอลเลกชัน ไม่ฟรี เมื่อคุณเห็นประเภทของค่าทำให้เกิดการสะสมจำนวนมากดังนั้นคุณต้องสรุป "uh-oh, Boxing มากเกินไป"

ปัญหาคือHashSet<T>ต้องมีIEqualityComparer<T>เพื่อให้งานสำเร็จลุล่วง EqualityComparer.Default<T>()เนื่องจากคุณไม่ได้ให้อย่างใดอย่างหนึ่งก็ต้องถอยกลับไปหนึ่งส่งกลับโดย เมธอดนั้นสามารถทำงานกับสตริงได้เป็นอย่างดีมันใช้ IEquatable แต่ไม่ใช่สำหรับ Point มันเป็นประเภทที่ harks จาก. NET 1.0 และไม่เคยได้รับความรักทั่วไป สิ่งที่สามารถทำได้คือใช้วิธีการของวัตถุ

ปัญหาอื่น ๆ คือ Point.GetHashCode () ไม่ได้ทำงานที่เป็นตัวเอกในการทดสอบนี้มีการชนกันมากเกินไปดังนั้นมันจึงส่งผลกระทบต่อ Object.Equals () ค่อนข้างหนัก String มีการใช้งาน GetHashCode ที่ยอดเยี่ยม

คุณสามารถแก้ไขปัญหาทั้งสองได้โดยการให้ HashSet ด้วยเครื่องมือเปรียบเทียบที่ดี ชอบสิ่งนี้:

class PointComparer : IEqualityComparer<Point> {
    public bool Equals(Point x, Point y) {
        return x.X == y.X && x.Y == y.Y;
    }

    public int GetHashCode(Point obj) {
        // Perfect hash for practical bitmaps, their width/height is never >= 65536
        return (obj.Y << 16) ^ obj.X;
    }
}

และใช้มัน:

HashSet<Point> list = new HashSet<Point>(new PointComparer());

และตอนนี้ก็เร็วขึ้นประมาณ 150 เท่าและเอาชนะการทดสอบสตริงได้อย่างง่ายดาย


26
+1 สำหรับการจัดทำเมธอด GetHashCode เพียงแค่อยากรู้อยากเห็นคุณมาพร้อมกับobj.X << 16 | obj.Y;การใช้งานโดยเฉพาะอย่างไร
Akash KC

32
มันได้รับแรงบันดาลใจจากวิธีที่เม้าส์ผ่านตำแหน่งในหน้าต่าง มันเป็นแฮชที่สมบูรณ์แบบสำหรับบิตแมปใด ๆ ที่คุณต้องการแสดง
Hans Passant

2
ดีที่ได้รู้. เอกสารใด ๆ หรือแนวทางที่ดีที่สุดในการเขียน hashcode เช่นคุณ? ที่จริงแล้วฉันยังอยากจะรู้ว่าแฮชโค้ดข้างต้นมาพร้อมกับประสบการณ์ของคุณหรือแนวทางที่คุณปฏิบัติตาม
Akash KC

5
@AkashKC ฉันไม่ค่อยมีประสบการณ์กับ C # แต่เท่าที่ฉันรู้ว่าจำนวนเต็มโดยทั่วไปคือ 32 บิต ในกรณีนี้คุณต้องการกัญชา 2 ตัวเลขและจากซ้ายขยับหนึ่ง 16bits คุณให้แน่ใจว่า "ลด" 16 บิตของแต่ละหมายเลขไม่ได้ "ส่งผลกระทบต่อ" อื่น ๆ |ที่มี สำหรับตัวเลข 3 ตัวคุณสามารถใช้ 22 และ 11 เป็นกะได้ สำหรับ 4 หมายเลขมันจะเป็น 24, 16, 8 อย่างไรก็ตามจะยังคงมีการชนกัน แต่ถ้าตัวเลขมีขนาดใหญ่ แต่มันก็สำคัญมากขึ้นอยู่กับการHashSetใช้งาน ถ้ามันใช้ open-adressing กับ "bit truncation" (ฉันไม่คิดว่ามันจะเป็นเช่นนั้น) แนวทางการเลื่อนซ้ายอาจไม่ดี
MSeifert

3
@HansPassant: ฉันสงสัยว่าการใช้ XOR แทน OR ใน GetHashCode อาจจะดีกว่าเล็กน้อย - ในกรณีที่พิกัดจุดอาจเกิน 16 บิต (อาจไม่ใช่ในจอแสดงผลทั่วไป แต่ในอนาคตอันใกล้) // XOR มักใช้ฟังก์ชันแฮชได้ดีกว่า OR เนื่องจากสูญเสียข้อมูลน้อยกว่าคือ reversibke ฯลฯ // เช่นหากพิกัดเชิงลบได้รับอนุญาตให้พิจารณาว่าเกิดอะไรขึ้นกับการสนับสนุน X หาก Y เป็นลบ
Krazy Glew

85

เหตุผลหลักสำหรับการลดลงของการแสดงคือการชกมวยทั้งหมดที่เกิดขึ้น (ตามที่อธิบายไว้แล้วในคำตอบของฮันส์แพสแทนท์ )

นอกจากนั้นอัลกอริทึมรหัสแฮชยิ่งทำให้ปัญหาแย่ลงเพราะจะทำให้มีการโทรEquals(object obj)เพิ่มขึ้นซึ่งจะเป็นการเพิ่มปริมาณการแปลงมวย

นอกจากนี้ทราบว่ารหัสกัญชาPointx ^ yคำนวณโดย สิ่งนี้ทำให้เกิดการกระจายตัวน้อยมากในช่วงข้อมูลของคุณดังนั้นจึงเป็นที่เก็บข้อมูลที่HashSetล้นเกิน - บางสิ่งที่ไม่ได้เกิดขึ้นstringเมื่อการกระจายตัวของแฮชมีขนาดใหญ่กว่ามาก

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

(x << 16) ^ y

สำหรับบางคำแนะนำที่ดีเมื่อมันมาถึงรหัสกัญชาอ่านบล็อกโพสต์เอริค Lippert ในเรื่อง


4
ดูที่แหล่งอ้างอิงของ Point the GetHashCodeperform: unchecked(x ^ y)ในขณะที่stringมันดูซับซ้อนกว่านี้ ..
Gilad Green

2
อืม .. เพื่อตรวจสอบว่าการสันนิษฐานของคุณถูกต้องฉันแค่ลองใช้HashSet<long>()แทนและใช้list.Add(unchecked(x ^ y));เพื่อเพิ่มค่าให้กับ HashSet นี้เป็นจริงได้เร็วขึ้นกว่าHashSet<string> (345 มิลลิวินาที) นี่แตกต่างจากสิ่งที่คุณอธิบายหรือไม่?
Ahmed Abdelhameed

4
@ AhmedAbdelhameed นั่นอาจเป็นเพราะคุณกำลังเพิ่มสมาชิกน้อยลงในชุดแฮชของคุณมากกว่าที่คุณรับรู้ (อีกครั้งเนื่องจากการกระจายขั้นตอนวิธีแฮชโค้ดที่น่ากลัว) การนับจำนวนlistเมื่อคุณเติมข้อมูลเสร็จแล้วคืออะไร
ระหว่าง

4
@AhmedAbdelhameed การทดสอบของคุณไม่ถูกต้อง คุณกำลังเพิ่มความยาวซ้ำไปซ้ำมาดังนั้นจริงๆแล้วมีเพียงไม่กี่องค์ประกอบที่คุณกำลังแทรกอยู่ เมื่อใส่pointที่HashSetภายในจะเรียกGetHashCodeและสำหรับแต่ละจุดเหล่านั้นแฮชโค้ดเดียวกันจะเรียกEqualsเพื่อตรวจสอบว่ามันมีอยู่แล้ว
Ofir Winegarten

49
ไม่มีความจำเป็นที่จะใช้เป็นPointเมื่อคุณสามารถสร้างชั้นที่ดำเนินการIEqualityComparer<Point>และเข้ากันได้เก็บกับสิ่งอื่น ๆ ที่ทำงานร่วมกับPointขณะที่ได้รับประโยชน์จากการที่ไม่ได้มีคนยากจนและจำเป็นที่จะต้องในกล่องGetHashCode Equals()
Jon Hanna
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.