จริง ๆ แล้วฉันพบว่าชุดภาชนะมาตรฐานส่วนใหญ่จะไร้ประโยชน์ตัวเองและชอบที่จะใช้อาร์เรย์ แต่ฉันทำมันในวิธีที่แตกต่างกัน
ในการคำนวณการตั้งค่าทางแยกฉันวนซ้ำแถวแรกและทำเครื่องหมายองค์ประกอบด้วยบิตเดียว จากนั้นฉันวนซ้ำแถวที่สองและมองหาองค์ประกอบที่ทำเครื่องหมายไว้ Voila ตั้งค่าการแยกในเวลาเชิงเส้นที่มีงานและหน่วยความจำน้อยกว่าโต๊ะแฮชเช่นสหภาพและความแตกต่างนั้นง่ายพอ ๆ กันที่จะใช้โดยใช้วิธีนี้ มันจะช่วยให้ codebase ของฉันหมุนรอบองค์ประกอบการทำดัชนีมากกว่าการทำซ้ำ (ฉันทำดัชนีดัชนีกับองค์ประกอบไม่ใช่ข้อมูลขององค์ประกอบเอง) และไม่ค่อยต้องการเรียงลำดับอะไร แต่ฉันไม่ได้ใช้โครงสร้างข้อมูลชุดในปีที่ผ่านมา ผลลัพธ์.
ฉันยังมีรหัส C ที่เล่นซอเล็กน้อยที่ฉันใช้แม้ว่าองค์ประกอบจะไม่มีเขตข้อมูลสำหรับวัตถุประสงค์ดังกล่าว มันเกี่ยวข้องกับการใช้หน่วยความจำขององค์ประกอบเองโดยการตั้งค่าบิตที่สำคัญที่สุด (ซึ่งฉันไม่เคยใช้) สำหรับการทำเครื่องหมายองค์ประกอบที่อยู่ภายใน นั่นเป็นขั้นต้นสวยอย่าทำอย่างนั้นเว้นแต่ว่าคุณจะทำงานในระดับใกล้การประกอบจริง ๆ แต่ก็อยากจะพูดถึงว่ามันสามารถใช้งานได้แม้ในกรณีที่องค์ประกอบไม่ได้ให้ข้อมูลเฉพาะสำหรับการสำรวจเส้นทางหากคุณสามารถรับประกันได้ บิตบางอย่างจะไม่ถูกใช้ มันสามารถคำนวณจุดตัดที่กำหนดระหว่างองค์ประกอบ 200 ล้าน (การแข่งขันข้อมูล 2.4 gigs) ในเวลาน้อยกว่าหนึ่งวินาทีใน i7 ของฉัน ลองทำชุดจุดตัดระหว่างสองstd::set
อินสแตนซ์ที่มีองค์ประกอบหนึ่งร้อยล้านรายการในเวลาเดียวกัน ไม่ได้เข้ามาใกล้
ที่นอกเหนือ ...
อย่างไรก็ตามฉันสามารถทำได้โดยการเพิ่มอิลิเมนต์แต่ละตัวในเวกเตอร์อื่นและตรวจสอบว่ามีอิลิเมนต์อยู่แล้ว
การตรวจสอบเพื่อดูว่าองค์ประกอบที่มีอยู่แล้วในเวกเตอร์ใหม่นั้นโดยทั่วไปจะเป็นการดำเนินการเชิงเส้นซึ่งจะทำให้การตัดกันชุดนั้นเป็นการทำงานแบบสองกำลังสอง (ปริมาณงานระเบิดที่ใหญ่กว่าขนาดอินพุต) ฉันแนะนำเทคนิคข้างต้นหากคุณต้องการใช้เวกเตอร์หรืออาร์เรย์แบบเก่าที่เรียบง่ายและทำในวิธีที่ปรับขนาดได้อย่างน่าอัศจรรย์
โดยพื้นฐาน: อัลกอริทึมชนิดใดที่ต้องมีชุดและไม่ควรทำกับประเภทคอนเทนเนอร์อื่น ๆ ?
ไม่มีถ้าคุณถามความเห็นของฉันที่มีอคติถ้าคุณกำลังพูดถึงเรื่องนี้ในระดับคอนเทนเนอร์ (เช่นในโครงสร้างข้อมูลที่ถูกนำมาใช้เพื่อให้การปฏิบัติการมีประสิทธิภาพโดยเฉพาะ) แต่มีจำนวนมากที่ต้องใช้ตรรกะชุดในระดับแนวคิด ตัวอย่างเช่นสมมติว่าคุณต้องการค้นหาสิ่งมีชีวิตในโลกของเกมที่มีความสามารถในการบินและว่ายน้ำและคุณมีสิ่งมีชีวิตที่บินได้ในชุดเดียว (ไม่ว่าคุณจะใช้ชุดภาชนะจริงหรือไม่ก็ตาม) . ในกรณีนั้นคุณต้องการจุดตัดที่กำหนด หากคุณต้องการสิ่งมีชีวิตที่สามารถบินได้หรือมีมนต์ขลังแล้วคุณใช้ชุดสหภาพ แน่นอนว่าคุณไม่จำเป็นต้องมีชุดของคอนเทนเนอร์ที่จะใช้สิ่งนี้และการใช้งานที่เหมาะสมที่สุดโดยทั่วไปไม่จำเป็นต้องมีหรือต้องการให้มีคอนเทนเนอร์ที่ออกแบบมาเป็นพิเศษ
ออกไปสัมผัส
เอาล่ะฉันได้รับคำถามที่ดีจาก JimmyJames เกี่ยวกับวิธีการตั้งสี่แยกนี้ มันค่อนข้างเบี่ยงเบนไปจากเรื่อง แต่ก็ดีฉันสนใจที่จะเห็นผู้คนจำนวนมากใช้วิธีการล่วงล้ำขั้นพื้นฐานนี้เพื่อกำหนดจุดตัดเพื่อที่พวกเขาจะไม่สร้างโครงสร้างเสริมทั้งหมดเช่นต้นไม้ไบนารีที่สมดุลและตารางแฮชเพียงเพื่อวัตถุประสงค์ในการปฏิบัติการ ดังที่ได้กล่าวถึงความต้องการขั้นพื้นฐานคือรายการองค์ประกอบการคัดลอกตื้น ๆ เพื่อให้พวกเขากำลังจัดทำดัชนีหรือชี้ไปที่องค์ประกอบที่ใช้ร่วมกันซึ่งสามารถ "ทำเครื่องหมาย" ตามการส่งผ่านโดยผ่านรายการแรกหรืออาร์เรย์ที่ไม่เรียงลำดับหรืออะไรก็ตาม ผ่านรายการที่สอง
อย่างไรก็ตามสิ่งนี้สามารถทำได้จริงในบริบทมัลติเธรดโดยไม่ต้องสัมผัสองค์ประกอบโดยที่:
- มวลรวมทั้งสองมีดัชนีองค์ประกอบ
- ช่วงของดัชนีไม่ใหญ่เกินไป (พูดว่า [0, 2 ^ 26), ไม่นับพันล้านหรือมากกว่า) และมีความหนาแน่นสูงพอสมควร
สิ่งนี้ทำให้เราสามารถใช้อาเรย์แบบขนาน (เพียงหนึ่งบิตต่อองค์ประกอบ) เพื่อวัตถุประสงค์ในการตั้งค่า แผนภาพ:
การซิงโครไนซ์เธรดต้องอยู่ที่นั่นเมื่อรับอาเรย์บิตแบบขนานจากพูลและปล่อยกลับไปยังพูล (ทำได้โดยปริยายเมื่ออยู่นอกขอบเขต) สองลูปจริงเพื่อดำเนินการตั้งค่าไม่จำเป็นต้องเกี่ยวข้องกับการซิงค์เธรดใด ๆ เราไม่จำเป็นต้องใช้พูลบิตแบบขนานหากเธรดสามารถจัดสรรและเพิ่มบิตในเครื่องได้ แต่บิตพูลนั้นมีประโยชน์ในการทำให้รูปแบบทั่วไปในโค้ดเบสที่เหมาะกับการแสดงข้อมูลชนิดนี้ซึ่งองค์ประกอบกลางมักจะถูกอ้างอิง โดยดัชนีเพื่อให้แต่ละเธรดไม่ต้องกังวลกับการจัดการหน่วยความจำที่มีประสิทธิภาพ ตัวอย่างที่เด่นชัดสำหรับพื้นที่ของฉันคือระบบส่วนประกอบและเอนทิตีแทนการจัดทำดัชนีตาข่าย ทั้งสองบ่อยครั้งต้องมีการตั้งค่าจุดแยกและมักจะอ้างถึงทุกสิ่งที่เก็บไว้ในส่วนกลาง (ส่วนประกอบและเอนทิตีใน ECS และจุดยอด, ขอบ,
หากดัชนีไม่ได้ถูกครอบครองอย่างหนาแน่นและกระจัดกระจายอยู่นี่ก็ยังคงสามารถนำไปใช้กับการดำเนินการที่เหมาะสมของอาร์เรย์บิตบิต / บูลีนแบบเบาบางเช่นเดียวกับที่เก็บหน่วยความจำในชิ้น 512 บิต (64 ไบต์ต่อโหนด ) และข้ามการจัดสรรบล็อคต่อเนื่องที่ว่างเปล่าอย่างสมบูรณ์ โอกาสที่คุณกำลังใช้สิ่งนี้อยู่แล้วหากโครงสร้างข้อมูลส่วนกลางของคุณถูกครอบครองโดยองค์ประกอบอย่างกระจัดกระจาย
... แนวคิดที่คล้ายคลึงกันสำหรับชุดบิตกระจัดกระจายเพื่อทำหน้าที่เป็นอาร์เรย์บิตแบบขนาน โครงสร้างเหล่านี้ยังให้ความสำคัญกับความไม่สามารถเปลี่ยนแปลงได้เนื่องจากมันง่ายต่อการคัดลอกบล็อกขนาดเล็กที่ไม่จำเป็นต้องคัดลอกแบบลึกเพื่อสร้างสำเนาที่ไม่เปลี่ยนรูปแบบใหม่
อีกครั้งตั้งค่าการแยกระหว่างองค์ประกอบหลายร้อยล้านสามารถทำได้ภายในไม่กี่วินาทีโดยใช้วิธีนี้ในเครื่องเฉลี่ยมากและที่อยู่ภายในหัวข้อเดียว
นอกจากนี้ยังสามารถทำได้ภายในครึ่งเวลาหากลูกค้าไม่ต้องการรายการองค์ประกอบสำหรับการแยกที่เกิดเช่นถ้าพวกเขาต้องการใช้ตรรกะบางอย่างกับองค์ประกอบที่พบในรายการทั้งสองจุดที่พวกเขาสามารถผ่าน ตัวชี้ฟังก์ชั่นหรือฟังก์ชั่นหรือตัวแทนหรือสิ่งที่จะถูกเรียกกลับไปช่วงกระบวนการขององค์ประกอบที่ตัดกัน สิ่งนี้มีผลกระทบ:
// 'func' receives a range of indices to
// process.
set_intersection(func):
{
parallel_bits = bit_pool.acquire()
// Mark the indices found in the first list.
for each index in list1:
parallel_bits[index] = 1
// Look for the first element in the second list
// that intersects.
first = -1
for each index in list2:
{
if parallel_bits[index] == 1:
{
first = index
break
}
}
// Look for elements that don't intersect in the second
// list to call func for each range of elements that do
// intersect.
for each index in list2 starting from first:
{
if parallel_bits[index] != 1:
{
func(first, index)
first = index
}
}
If first != list2.num-1:
func(first, list2.num)
}
... หรือบางสิ่งบางอย่างในลักษณะนี้ ส่วนที่แพงที่สุดของ pseudocode ในไดอะแกรมแรกนั้นอยู่intersection.append(index)
ในลูปที่สองและจะใช้std::vector
กับขนาดของรายการขนาดเล็กล่วงหน้าด้วย
จะทำอย่างไรถ้าฉันทำสำเนาลึกลงไป
หยุดนั่นสิ! หากคุณต้องการตั้งค่าทางแยกมันก็หมายความว่าคุณกำลังทำซ้ำข้อมูลเพื่อตัดกัน โอกาสที่แม้กระทั่งวัตถุที่เล็กที่สุดของคุณก็ไม่เล็กกว่าดัชนี 32 บิต เป็นไปได้มากที่จะลดช่วงการกำหนดตำแหน่งขององค์ประกอบของคุณเป็น 2 ^ 32 (2 ^ 32 องค์ประกอบไม่ใช่ 2 ^ 32 ไบต์) เว้นแต่ว่าคุณต้องการองค์ประกอบมากกว่า 4.3 พันล้านรายการทันทีที่ต้องการโซลูชันที่แตกต่างกันโดยสิ้นเชิง ( และแน่นอนว่าไม่ได้ใช้ชุดคอนเทนเนอร์ในหน่วยความจำ)
คีย์ตรงกัน
ในกรณีที่เราต้องทำการตั้งค่าการทำงานที่องค์ประกอบไม่เหมือนกัน แต่อาจมีคีย์ที่ตรงกันได้ ในกรณีนั้นความคิดเดียวกันข้างต้น เราเพียงต้องการแมปรหัสที่ไม่ซ้ำกันกับดัชนี หากคีย์เป็นสตริงตัวอย่างเช่นสตริง interned สามารถทำได้ ในกรณีเหล่านี้โครงสร้างข้อมูลที่ดีเช่น trie หรือตารางแฮชถูกเรียกเพื่อแม็พคีย์สตริงกับดัชนี 32- บิต แต่เราไม่ต้องการโครงสร้างดังกล่าวเพื่อทำการตั้งค่าบนดัชนี 32- บิตที่เกิดขึ้น
โซลูชันอัลกอริทึมและโครงสร้างข้อมูลราคาถูกและตรงไปตรงมาจำนวนมากเปิดขึ้นเช่นนี้เมื่อเราสามารถทำงานกับดัชนีองค์ประกอบในช่วงที่สมเหตุสมผลมากไม่ใช่ช่วงการกำหนดแอดเดรสแบบเต็มของเครื่องและดังนั้นจึงมักจะคุ้มค่ากว่า สามารถรับดัชนีที่ไม่ซ้ำกันสำหรับแต่ละคีย์ที่ไม่ซ้ำกัน
ฉันรักดัชนี!
ฉันรักดัชนีเช่นเดียวกับพิซซ่าและเบียร์ เมื่อฉันอายุ 20 ปีฉันได้เป็น C ++ และเริ่มออกแบบโครงสร้างข้อมูลที่สอดคล้องกับมาตรฐานทุกชนิด (รวมถึงเทคนิคที่เกี่ยวข้องกับการลดความซับซ้อนของ ctor จากช่วง ctor ในเวลารวบรวม) เมื่อมองย้อนกลับไปนั้นเป็นการเสียเวลามาก
หากคุณหมุนฐานข้อมูลของคุณรอบ ๆ องค์ประกอบการจัดเก็บแบบรวมศูนย์ในอาร์เรย์และการจัดทำดัชนีพวกเขาแทนที่จะเก็บไว้ในลักษณะที่มีการแยกส่วนและอาจข้ามช่วงที่อยู่แอดเดรสทั้งหมดของเครื่องคุณสามารถสิ้นสุดการสำรวจโลกแห่งความเป็นไปได้ ภาชนะบรรจุและขั้นตอนวิธีการออกแบบที่หมุนรอบเก่าธรรมดาหรือint
int32_t
และฉันพบว่าผลลัพธ์จะมีประสิทธิภาพมากขึ้นและง่ายต่อการบำรุงรักษามากขึ้นโดยที่ฉันไม่ได้ถ่ายโอนองค์ประกอบจากโครงสร้างข้อมูลหนึ่งไปยังอีกโครงสร้างหนึ่งอย่างต่อเนื่อง
ตัวอย่างบางกรณีใช้กรณีที่คุณสามารถสันนิษฐานได้ว่าค่าเฉพาะใด ๆ ของT
มีดัชนีที่ไม่ซ้ำกันและจะมีอินสแตนซ์ที่อยู่ในอาร์เรย์ส่วนกลาง:
ประเภท Radix มัลติเธรดซึ่งทำงานได้ดีกับจำนวนเต็มไม่ได้ลงนามดัชนี จริง ๆ แล้วฉันมีการจัดเรียง radix แบบมัลติเธรดซึ่งใช้เวลาประมาณ 1 ใน 10 ของเวลาในการจัดเรียงองค์ประกอบต่างๆนับร้อยล้านรายการในแบบเรียงลำดับขนานของ Intel และ Intel นั้นเร็วกว่าstd::sort
การป้อนข้อมูลขนาดใหญ่ถึง4 เท่า แน่นอนว่า Intel นั้นมีความยืดหยุ่นมากกว่าเนื่องจากเป็นแบบเรียงตามการเปรียบเทียบและสามารถเรียงลำดับตามพจนานุกรมได้ดังนั้นจึงเป็นการเปรียบเทียบแอปเปิ้ลกับส้ม แต่ที่นี่ฉันมักจะต้องการส้มเท่านั้นเช่นฉันอาจใช้ Radix sort Pass เพียงเพื่อให้ได้รูปแบบการเข้าถึงหน่วยความจำที่เป็นมิตรกับแคชหรือกรองการทำซ้ำอย่างรวดเร็ว
ความสามารถในการสร้างการเชื่อมโยงโครงสร้างเช่นรายการที่เชื่อมโยง, ต้นไม้, กราฟแยกตารางผูกมัดกัญชา ฯลฯ โดยไม่ต้องจัดสรรกองต่อโหนด เราสามารถจัดสรรโหนดเป็นกลุ่มขนานกับองค์ประกอบและเชื่อมโยงมันเข้ากับดัชนี โหนดเองกลายเป็นดัชนีแบบ 32 บิตไปยังโหนดถัดไปและเก็บไว้ในอาร์เรย์ขนาดใหญ่เช่น:
เป็นมิตรสำหรับการประมวลผลแบบขนาน บ่อยครั้งที่โครงสร้างที่เชื่อมโยงนั้นไม่ค่อยเป็นมิตรสำหรับการประมวลผลแบบขนานเนื่องจากอย่างน้อยก็น่าอึดอัดใจที่จะพยายามทำให้เกิดการขนานในต้นไม้หรือการข้ามผ่านรายการที่เชื่อมโยงซึ่งตรงกันข้ามกับการพูดเพียงแค่ทำขนานสำหรับวนรอบอาร์เรย์ ด้วยการเป็นตัวแทนดัชนี / กลางอาเรย์เราสามารถไปที่อาเรย์กลางนั้นและประมวลผลทุกอย่างในลูปคู่ขนานที่เป็นก้อน เรามีอาร์เรย์ส่วนกลางขององค์ประกอบทั้งหมดที่เราสามารถประมวลผลด้วยวิธีนี้แม้ว่าเราจะต้องการประมวลผลบางอย่างเท่านั้น (ณ จุดนี้คุณอาจประมวลผลองค์ประกอบที่จัดทำดัชนีโดยรายการที่จัดเรียง Radix เพื่อให้เข้าถึงแคชได้ง่ายผ่านอาร์เรย์ส่วนกลาง)
สามารถเชื่อมโยงข้อมูลไปยังแต่ละองค์ประกอบในการบินคงที่ในเวลา เช่นเดียวกับกรณีของอาร์เรย์ขนานของบิตด้านบนเราสามารถเชื่อมโยงข้อมูลแบบขนานกับองค์ประกอบสำหรับพูดประมวลผลชั่วคราวได้อย่างง่ายดายและราคาถูกมาก กรณีนี้มีการใช้งานมากกว่าข้อมูลชั่วคราว ตัวอย่างเช่นระบบตาข่ายอาจต้องการอนุญาตให้ผู้ใช้แนบแผนที่ UV จำนวนมากไปยังตาข่ายตามที่พวกเขาต้องการ ในกรณีเช่นนี้เราไม่สามารถเขียนโค้ดยาก ๆ ได้ว่ามีแผนที่ UV จำนวนกี่จุดในแต่ละจุดยอดและเผชิญหน้าโดยใช้วิธี AoS เราจำเป็นต้องสามารถเชื่อมโยงข้อมูลดังกล่าวได้ในทันทีและอาร์เรย์แบบคู่ขนานนั้นมีประโยชน์และราคาถูกกว่าคอนเทนเนอร์ที่เชื่อมโยงที่ซับซ้อนทุกชนิดแม้แต่แฮชตาราง
แน่นอนว่าอาร์เรย์แบบขนานจะถูกดึงออกมาเนื่องจากลักษณะข้อผิดพลาดที่เกิดขึ้นได้ง่ายในการรักษาอาร์เรย์แบบขนานในการซิงค์ซึ่งกันและกัน เมื่อใดก็ตามที่เราลบองค์ประกอบที่ดัชนี 7 ออกจากอาร์เรย์ "root" เราก็ต้องทำสิ่งเดียวกันสำหรับ "children" อย่างไรก็ตามมันง่ายพอที่ในภาษาส่วนใหญ่ที่จะทำให้แนวความคิดนี้เป็นที่เก็บวัตถุประสงค์ทั่วไปเพื่อให้ตรรกะที่ยุ่งยากในการรักษาอาร์เรย์ขนานในการซิงค์กับแต่ละอื่น ๆ จะต้องมีอยู่ในที่เดียวตลอดทั้ง codebase ทั้งหมดและคอนเทนเนอร์อาร์เรย์แบบขนานสามารถ ใช้การดำเนินการของ sparse array ด้านบนเพื่อหลีกเลี่ยงการสูญเสียความทรงจำมากมายสำหรับพื้นที่ว่างที่ต่อเนื่องกันในอาร์เรย์ที่จะเรียกคืนเมื่อมีการแทรกครั้งต่อไป
เพิ่มเติมอย่างประณีต: Spits Bitset Tree
เอาล่ะฉันได้รับการร้องขอให้ทำอย่างละเอียดมากกว่านี้ซึ่งฉันคิดว่ามันเหน็บแนม แต่ฉันจะทำอย่างนั้นต่อไปเพราะมันสนุกมาก! หากผู้คนต้องการที่จะนำความคิดนี้ไปสู่ระดับใหม่ทั้งหมดก็เป็นไปได้ที่จะดำเนินการทางแยกชุดโดยไม่ต้องวนซ้ำเชิงเส้นผ่านองค์ประกอบ N + M นี่คือโครงสร้างข้อมูลที่ดีที่สุดของฉันที่ฉันใช้มานานและโดยทั่วไปแล้วset<int>
:
เหตุผลที่สามารถดำเนินการแยกชุดโดยไม่ต้องตรวจสอบแต่ละองค์ประกอบในรายการทั้งสองเป็นเพราะบิตชุดเดียวที่รากของลำดับชั้นสามารถระบุได้ว่าพูดล้านองค์ประกอบที่อยู่ติดกันในชุด เพียงแค่ตรวจสอบหนึ่งบิตเราสามารถรู้ได้ว่าดัชนี N ในช่วงนั้น[first,first+N)
อยู่ในเซตซึ่ง N อาจมีจำนวนมาก
ฉันใช้สิ่งนี้เป็นเครื่องมือเพิ่มประสิทธิภาพแบบวนรอบเมื่อสำรวจดัชนีที่อยู่เนื่องจากว่ามี 8 ล้านดัชนีอยู่ในชุด ปกติแล้วเราต้องเข้าถึงจำนวนเต็ม 8 ล้านในหน่วยความจำในกรณีนั้น ด้วยสิ่งนี้มันสามารถตรวจสอบเพียงไม่กี่บิตและเกิดขึ้นกับช่วงดัชนีของดัชนีที่ถูกครอบครองเพื่อวนรอบ นอกจากนี้ช่วงของดัชนีที่เกิดขึ้นนั้นอยู่ในลำดับที่เรียงลำดับซึ่งทำให้การเข้าถึงตามลำดับที่เป็นมิตรกับแคชมากเมื่อเทียบกับพูดซ้ำผ่านแถวลำดับดัชนีที่ไม่เรียงลำดับซึ่งใช้ในการเข้าถึงข้อมูลองค์ประกอบดั้งเดิม แน่นอนว่าเทคนิคนี้ค่าโดยสารแย่กว่ามากสำหรับกรณีที่มีผู้กระจัดกระจายมากและสถานการณ์ที่เลวร้ายที่สุดนั้นเป็นเหมือนดัชนีเดี่ยวทุกตัวที่มีค่าเป็นเลขคู่ แต่ในกรณีการใช้งานของฉันอย่างน้อย