วิธีที่เร็วที่สุดในการจัดเรียง 10 หมายเลข? (ตัวเลข 32 บิต)


211

ฉันกำลังแก้ปัญหาและมันเกี่ยวข้องกับการเรียงลำดับหมายเลข 10 (int32) อย่างรวดเร็ว แอปพลิเคชันของฉันต้องเรียงลำดับหมายเลข 10 ล้านครั้งโดยเร็วที่สุด ฉันสุ่มตัวอย่างชุดข้อมูลขององค์ประกอบหลายพันล้านรายการและทุกครั้งที่ฉันต้องเลือกตัวเลข 10 ตัวจากนั้นทำการย่อตัวและจัดเรียง (และทำการสรุปจากรายการองค์ประกอบเรียง 10)

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

ไม่มีใครมีความคิดเกี่ยวกับวิธีการแก้ไขปัญหานี้หรือไม่?


14
อย่างที่มันฟังดูชุดifข้อความที่ซ้อนกันน่าจะดีที่สุด หลีกเลี่ยงการวนซ้ำ
John Alexiou

8
คุณคาดหวังหรือไม่ว่าตัวเลขนั้นจะมอบให้คุณเมื่อมีอคติในชุดการเรียงสับเปลี่ยนหรือพวกมันจะกระจายอย่างสม่ำเสมอหรือไม่? จะมีความสัมพันธ์ใด ๆ ระหว่างการเรียงลำดับของรายการหนึ่งกับอีกรายการหรือไม่?
Douglas Zare

4
ชุดข้อมูลทั้งหมด (ที่มีตัวเลขเป็นพันล้าน) ถูกแจกจ่ายตามกฎหมายของ Benford แต่เมื่อฉันเลือกองค์ประกอบแบบสุ่มจากชุดนี้พวกเขาจะไม่เป็นอีกต่อไป (ฉันคิดว่า)
bodacydo

13
คุณอาจต้องการอ่านstackoverflow.com/q/2786899/995714
phuclv

11
หากคุณเลือกแบบสุ่มจากองค์ประกอบหลายพันล้านครั้งอาจเป็นไปได้ว่าเวลาแฝงสำหรับการดึงข้อมูลนั้นอาจมีผลกระทบมากกว่าเวลาที่ต้องใช้ในการจัดเรียงองค์ประกอบที่เลือกแม้ว่าชุดข้อมูลทั้งหมดจะอยู่ใน RAM คุณสามารถทดสอบผลกระทบโดยการเปรียบเทียบประสิทธิภาพการเลือกข้อมูลตามลำดับกับแบบสุ่ม
Steve S.

คำตอบ:


213

(ติดตามข้อเสนอแนะของ HelloWorld เพื่อค้นหาเครือข่ายการเรียงลำดับ)

ดูเหมือนว่าเครือข่าย 29-เปรียบเทียบ / swap เป็นวิธีที่เร็วที่สุดในการเรียงลำดับ 10 อินพุต ฉันใช้เครือข่ายที่ค้นพบโดย Waksman ในปี 1969 สำหรับตัวอย่างนี้ใน Javascript ซึ่งควรแปลเป็น C โดยตรงเนื่องจากเป็นเพียงรายการของifข้อความการเปรียบเทียบและการแลกเปลี่ยน

function sortNet10(data) {	// ten-input sorting network by Waksman, 1969
    var swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
    return(data);
}

alert(sortNet10([5,7,1,8,4,3,6,9,2,0]));

นี่คือการแสดงกราฟิกของเครือข่ายแบ่งออกเป็นขั้นตอนอิสระ เพื่อใช้ประโยชน์จากการประมวลผลแบบขนานสามารถจัดกลุ่ม 5-4-3-4-4-4-3-2 เป็นกลุ่ม 4-4-4-4-4-4-3-3-2
เครือข่ายคัดแยก 10 อินพุต (Waksman, 1969)

เครือข่ายคัดแยก 10 อินพุต (Waksman, 1969) จัดกลุ่มใหม่


69
ข้อเสนอแนะ; ใช้มาโครแลกเปลี่ยน ชอบ#define SORTPAIR(data, i1, i2) if (data[i1] > data[i2]) { int swap = data[i1]... }
Peter Cordes

9
มันสามารถแสดงเหตุผลว่านี่เป็นขั้นต่ำหรือไม่
corsiKa

8
@corsiKa ใช่แล้วเครือข่ายการเรียงลำดับเป็นพื้นที่ของการวิจัยมาตั้งแต่ยุคแรก ๆ ของวิทยาศาสตร์คอมพิวเตอร์ ในหลายกรณีการแก้ปัญหาที่ดีที่สุดเป็นที่รู้จักกันมานานหลายทศวรรษ ดูen.wikipedia.org/wiki/Sorting_network
m69 '' น่าสะอิดสะเอียนและไม่ไหวติง ''

8
ฉันสร้าง Jsperf เพื่อทดสอบและฉันสามารถยืนยันได้ว่า Network Sort เร็วกว่า 20 เท่าที่การจัดเรียงดั้งเดิมของเบราว์เซอร์ jsperf.com/fastest-10-number-sort
Daniel

9
@Katai สิ่งนี้จะทำลายการเพิ่มประสิทธิภาพใด ๆ ที่คอมไพเลอร์ของคุณอาจสร้าง ความคิดที่ไม่ดี อ่านสิ่งนี้สำหรับข้อมูลเพิ่มเติมen.wikipedia.org/wiki/…
Antzi

88

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

การเรียงลำดับ Bitonicเป็นการดำเนินการของเครือข่ายดังกล่าว อันนี้ใช้ได้ดีที่สุดกับ len (n) <= 32 บน CPU ในอินพุตที่ใหญ่กว่าคุณอาจนึกถึงการย้ายไปใช้ GPU https://en.wikipedia.org/wiki/Sorting_network

Btw หน้าดีเพื่อเปรียบเทียบขั้นตอนวิธีการเรียงลำดับนี้เป็นหนึ่งในที่นี่ bitonic sort(แม้ว่ามันหายไป

http://www.sorting-algorithms.com


3
@ ErickG.Hagstrom มีวิธีแก้ไขอยู่มากมาย ตราบใดที่พวกเขาใช้การเปรียบเทียบ 29 รายการพวกเขาก็มีประสิทธิภาพเท่าเทียมกัน ฉันใช้วิธีแก้ปัญหาของ Waksman ตั้งแต่ปี 1969 เห็นได้ชัดว่าเขาเป็นคนแรกที่ค้นพบรุ่นเปรียบเทียบ 29
m69 '' น่าสะอิดสะเอียนและไม่รู้จัก ''

1
ใช่ @ m69 มีมากกว่าหนึ่งล้าน โซลูชันของ Waksman มีความยาว 29 และความลึก 9 การแก้ปัญหาที่ฉันเชื่อมโยงคือการปรับปรุงในมิติความลึก: length = 29, depth = 8 แน่นอนเมื่อใช้งานใน C ความลึกไม่สำคัญ
Erick G. Hagstrom

4
@ ErickG.Hagstrom เห็นได้ชัดว่ามี 87 โซลูชั่นที่มีความลึก 7 ซึ่งเป็นครั้งแรกที่ Knuth ค้นพบในปี 1973 แต่ฉันไม่สามารถหาพวกเขาด้วย Google อย่างรวดเร็ว larc.unt.edu/ian/pubs/9-input.pdf (ดูข้อสรุปหน้า 14)
m69 '' การต่อว่าต่อขานและหลุดพ้น '24

4
@ ErickG.Hagstrom: ความลึกอาจไม่มีความแตกต่าง "ที่ระดับ C" แต่สันนิษฐานว่าเมื่อคอมไพเลอร์และซีพียูเสร็จสิ้นแล้วมีความเป็นไปได้ที่มันจะถูกต่อขนานภายในซีพียูบางส่วนและทำให้ความลึกขนาดเล็กลงอาจช่วยได้ แน่นอนขึ้นอยู่กับ CPU: CPU บางตัวค่อนข้างเรียบง่ายและทำสิ่งหนึ่งต่อจากนั้นในขณะที่ CPU บางตัวสามารถทำการบินหลายเที่ยวบินโดยเฉพาะอย่างยิ่งคุณอาจได้รับประสิทธิภาพที่แตกต่างกันมากสำหรับการโหลดและการจัดเก็บใด ๆ เพื่อจัดการ 10 ตัวแปรขึ้นอยู่กับว่าพวกเขาทำ
Steve Jessop

1
@ ErickG.Hagstrom มันไม่ชัดเจนจากหนังสือพิมพ์โดย Ian Parberry แต่เครือข่ายความลึก 7 มีความยาวมากกว่า 29 ดู Knuth, "The Art Of Computer Programming Vol.III", §5.3.4, รูปที่ . 49 และ 51
m69 '' น่าสะอิดสะเอียนและไม่รู้จัก ''

33

ใช้เครือข่ายการเรียงลำดับที่มีการเปรียบเทียบในกลุ่ม 4 ดังนั้นคุณสามารถทำได้ในการลงทะเบียน SIMD คู่ของคำสั่ง min / max ที่บรรจุใช้ฟังก์ชันตัวเปรียบเทียบที่บรรจุไว้ ขออภัยตอนนี้ฉันไม่มีเวลาค้นหาหน้าเว็บที่ฉันจำได้เกี่ยวกับสิ่งนี้ แต่หวังว่าการค้นหาในเครือข่ายการเรียงลำดับ SIMD หรือ SSE จะทำให้บางสิ่งเกิดขึ้น

x86 SSE มีคำสั่ง -32 บิตจำนวนเต็มและคำสั่งสูงสุดสำหรับเวกเตอร์ของสี่ 32 บิต ints AVX2 (Haswell และใหม่กว่า) มีเหมือนกัน แต่สำหรับ 256b ของ 8 ints นอกจากนี้ยังมีคำแนะนำการสลับที่มีประสิทธิภาพ

หากคุณมีเรียงลำดับอิสระขนาดเล็กจำนวนมากอาจเป็นไปได้ที่จะทำ 4 หรือ 8 เรียงลำดับแบบขนานโดยใช้เวกเตอร์ Esp หากคุณเลือกองค์ประกอบแบบสุ่ม (ดังนั้นข้อมูลที่จะเรียงลำดับจะไม่ต่อเนื่องกันในหน่วยความจำ) คุณสามารถหลีกเลี่ยงการสับและเพียงเปรียบเทียบตามลำดับที่คุณต้องการ 10 ลงทะเบียนเพื่อเก็บข้อมูลทั้งหมดจาก 4 (AVX2: 8) รายการ 10 ints ยังคงทิ้ง 6 regs สำหรับพื้นที่รอยขีดข่วน

เครือข่ายการเรียงลำดับ Vector จะมีประสิทธิภาพน้อยลงหากคุณต้องการเรียงลำดับข้อมูลที่เกี่ยวข้อง ในกรณีนั้นวิธีที่มีประสิทธิภาพที่สุดดูเหมือนว่าจะใช้การเปรียบเทียบแบบแพ็คเพื่อรับมาสก์ที่มีองค์ประกอบเปลี่ยนแปลงและใช้มาสก์นั้นเพื่อผสมผสานเวกเตอร์ของข้อมูลที่เกี่ยวข้อง (อ้างอิงถึง)


26

สิ่งที่เกี่ยวกับการเรียงลำดับการเลือกที่ไม่ได้ลงทะเบียนและไม่มีสาขา?

#include <iostream>
#include <algorithm>
#include <random>

//return the index of the minimum element in array a
int min(const int * const a) {
  int m = a[0];
  int indx = 0;
  #define TEST(i) (m > a[i]) && (m = a[i], indx = i ); 
  //see http://stackoverflow.com/a/7074042/2140449
  TEST(1);
  TEST(2);
  TEST(3);
  TEST(4);
  TEST(5);
  TEST(6);
  TEST(7);
  TEST(8);
  TEST(9);
  #undef TEST
  return indx;
}

void sort( int * const a ){
  int work[10];
  int indx;
  #define GET(i) indx = min(a); work[i] = a[indx]; a[indx] = 2147483647; 
  //get the minimum, copy it to work and set it at max_int in a
  GET(0);
  GET(1);
  GET(2);
  GET(3);
  GET(4);
  GET(5);
  GET(6);
  GET(7);
  GET(8);
  GET(9);
  #undef GET
  #define COPY(i) a[i] = work[i];
  //copy back to a
  COPY(0);
  COPY(1);
  COPY(2);
  COPY(3);
  COPY(4);
  COPY(5);
  COPY(6);
  COPY(7);
  COPY(8);
  COPY(9);
  #undef COPY
}

int main() {
  //generating and printing a random array
  int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
  std::random_device rd;
  std::mt19937 g(rd());
  std::shuffle( a, a+10, g);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  }
  std::cout << std::endl;

  //sorting and printing again
  sort(a);
  for (int i = 0; i < 10; i++) {
    std::cout << a[i] << ' ';
  } 

  return 0;
}

http://coliru.stacked-crooked.com/a/71e18bc4f7fa18c6

เส้นที่เกี่ยวข้องเพียง #defineแต่เป็นครั้งแรกที่สอง

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


เกณฑ์มาตรฐาน

ฉันเปรียบเทียบกับเครือข่ายการเรียงลำดับและรหัสของฉันดูเหมือนจะช้ากว่า อย่างไรก็ตามฉันพยายามที่จะลบการ unrolling และการคัดลอก ใช้รหัสนี้:

#include <iostream>
#include <algorithm>
#include <random>
#include <chrono>

int min(const int * const a, int i) {
  int m = a[i];
  int indx = i++;
  for ( ; i<10; i++) 
    //see http://stackoverflow.com/a/7074042/2140449
    (m > a[i]) && (m = a[i], indx = i ); 
  return indx;
}

void sort( int * const a ){
  for (int i = 0; i<9; i++)
    std::swap(a[i], a[min(a,i)]); //search only forward
}


void sortNet10(int * const data) {  // ten-input sorting network by Waksman, 1969
    int swap;
    if (data[0] > data[5]) { swap = data[0]; data[0] = data[5]; data[5] = swap; }
    if (data[1] > data[6]) { swap = data[1]; data[1] = data[6]; data[6] = swap; }
    if (data[2] > data[7]) { swap = data[2]; data[2] = data[7]; data[7] = swap; }
    if (data[3] > data[8]) { swap = data[3]; data[3] = data[8]; data[8] = swap; }
    if (data[4] > data[9]) { swap = data[4]; data[4] = data[9]; data[9] = swap; }
    if (data[0] > data[3]) { swap = data[0]; data[0] = data[3]; data[3] = swap; }
    if (data[5] > data[8]) { swap = data[5]; data[5] = data[8]; data[8] = swap; }
    if (data[1] > data[4]) { swap = data[1]; data[1] = data[4]; data[4] = swap; }
    if (data[6] > data[9]) { swap = data[6]; data[6] = data[9]; data[9] = swap; }
    if (data[0] > data[2]) { swap = data[0]; data[0] = data[2]; data[2] = swap; }
    if (data[3] > data[6]) { swap = data[3]; data[3] = data[6]; data[6] = swap; }
    if (data[7] > data[9]) { swap = data[7]; data[7] = data[9]; data[9] = swap; }
    if (data[0] > data[1]) { swap = data[0]; data[0] = data[1]; data[1] = swap; }
    if (data[2] > data[4]) { swap = data[2]; data[2] = data[4]; data[4] = swap; }
    if (data[5] > data[7]) { swap = data[5]; data[5] = data[7]; data[7] = swap; }
    if (data[8] > data[9]) { swap = data[8]; data[8] = data[9]; data[9] = swap; }
    if (data[1] > data[2]) { swap = data[1]; data[1] = data[2]; data[2] = swap; }
    if (data[3] > data[5]) { swap = data[3]; data[3] = data[5]; data[5] = swap; }
    if (data[4] > data[6]) { swap = data[4]; data[4] = data[6]; data[6] = swap; }
    if (data[7] > data[8]) { swap = data[7]; data[7] = data[8]; data[8] = swap; }
    if (data[1] > data[3]) { swap = data[1]; data[1] = data[3]; data[3] = swap; }
    if (data[4] > data[7]) { swap = data[4]; data[4] = data[7]; data[7] = swap; }
    if (data[2] > data[5]) { swap = data[2]; data[2] = data[5]; data[5] = swap; }
    if (data[6] > data[8]) { swap = data[6]; data[6] = data[8]; data[8] = swap; }
    if (data[2] > data[3]) { swap = data[2]; data[2] = data[3]; data[3] = swap; }
    if (data[4] > data[5]) { swap = data[4]; data[4] = data[5]; data[5] = swap; }
    if (data[6] > data[7]) { swap = data[6]; data[6] = data[7]; data[7] = swap; }
    if (data[3] > data[4]) { swap = data[3]; data[3] = data[4]; data[4] = swap; }
    if (data[5] > data[6]) { swap = data[5]; data[5] = data[6]; data[6] = swap; }
}


std::chrono::duration<double> benchmark( void(*func)(int * const), const int seed ) {
  std::mt19937 g(seed);
  int a[10] = {10,11,12,13,14,15,16,17,18,19};
  std::chrono::high_resolution_clock::time_point t1, t2; 
  t1 = std::chrono::high_resolution_clock::now();
  for (long i = 0; i < 1e7; i++) {
    std::shuffle( a, a+10, g);
    func(a);
  }
  t2 = std::chrono::high_resolution_clock::now();
  return std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
}

int main() {
  std::random_device rd;
  for (int i = 0; i < 10; i++) {
    const int seed = rd();
    std::cout << "seed = " << seed << std::endl;
    std::cout << "sortNet10: " << benchmark(sortNet10, seed).count() << std::endl;
    std::cout << "sort:      " << benchmark(sort,      seed).count() << std::endl;
  }
  return 0;
}

ฉันได้รับผลลัพธ์ที่ดีขึ้นอย่างต่อเนื่องสำหรับการจัดเรียงการเลือกสาขาน้อยกว่าเมื่อเทียบกับเครือข่ายการเรียงลำดับ

$ gcc -v
gcc version 5.2.0 (GCC) 
$ g++ -std=c++11 -Ofast sort.cpp && ./a.out
seed = -1727396418
sortNet10: 2.24137
sort:      2.21828
seed = 2003959850
sortNet10: 2.23914
sort:      2.21641
seed = 1994540383
sortNet10: 2.23782
sort:      2.21778
seed = 1258259982
sortNet10: 2.25199
sort:      2.21801
seed = 1821086932
sortNet10: 2.25535
sort:      2.2173
seed = 412262735
sortNet10: 2.24489
sort:      2.21776
seed = 1059795817
sortNet10: 2.29226
sort:      2.21777
seed = -188551272
sortNet10: 2.23803
sort:      2.22996
seed = 1043757247
sortNet10: 2.2503
sort:      2.23604
seed = -268332483
sortNet10: 2.24455
sort:      2.24304

4
ผลลัพธ์ไม่น่าประทับใจมาก แต่จริงๆแล้วสิ่งที่ฉันคาดหวัง เครือข่ายการเรียงลำดับช่วยลดการเปรียบเทียบไม่ให้สลับกัน เมื่อค่าทั้งหมดอยู่ในการเปรียบเทียบแคชแล้วราคาถูกกว่าการแลกเปลี่ยนมากดังนั้นการเรียงลำดับการเลือก (ซึ่งจะลดจำนวนการสลับ) ให้ต่ำลงจะเป็นมือที่สูงกว่า (และมีการเปรียบเทียบไม่มากนัก: เครือข่ายที่มี 29 compasions, สูงสุด 29 swaps ?; เทียบกับการเรียงลำดับการเลือกที่มีการเปรียบเทียบ 45 ครั้งและมากที่สุด 9 swaps)
ตัวอย่าง

7
โอ้และมันมีกิ่ง - ยกเว้นว่าเส้นfor ( ; i<10; i++) (m > a[i]) && (m = a[i], indx = i ); นั้นได้รับการปรับให้เหมาะสมเป็นพิเศษ (โดยทั่วไปการลัดวงจรเป็นรูปแบบของการแตกแขนง)
ตัวอย่าง

1
@EugeneRyabtsev ด้วยเช่นกัน แต่ถูกป้อนด้วยลำดับสุ่มที่เหมือนกันทุกครั้งดังนั้นจึงควรยกเลิก ผมพยายามที่จะเปลี่ยนด้วยstd::shuffle for (int n = 0; n<10; n++) a[n]=g();เวลาดำเนินการจะลดลงครึ่งหนึ่งและเครือข่ายเร็วขึ้นในขณะนี้
DarioP

สิ่งนี้จะเปรียบเทียบกับ libc ++ ได้std::sortอย่างไร
gnzlbg

1
@gnzlbg ฉันพยายามstd::sortเช่นกัน แต่มันก็ทำงานได้แย่มากจนฉันไม่ได้รวมไว้ในเกณฑ์มาตรฐาน ฉันเดาว่าด้วยชุดข้อมูลขนาดเล็กมีค่าใช้จ่ายค่อนข้าง
DarioP

20

คำถามไม่ได้บอกว่านี่เป็นแอปพลิเคชันบนเว็บบางประเภท สิ่งหนึ่งที่สะดุดตาฉันคือ:

ฉันสุ่มตัวอย่างชุดข้อมูลขององค์ประกอบหลายพันล้านรายการและทุกครั้งที่ฉันต้องเลือกตัวเลข 10 ตัวจากนั้นทำการย่อตัวและจัดเรียง (และทำการสรุปจากรายการองค์ประกอบเรียง 10)

ในฐานะที่เป็นวิศวกรซอฟต์แวร์และฮาร์ดแวร์สิ่งนี้ร้อง"FPGA"ให้ฉันอย่างแน่นอน ผมไม่ทราบว่าสิ่งที่ชนิดของข้อสรุปที่คุณจำเป็นต้องวาดจากชุดที่เรียงลำดับของตัวเลขหรือข้อมูลที่มาจาก แต่ฉันรู้ว่ามันคงจะน่ารำคาญกับกระบวนการบางระหว่างหนึ่งร้อยล้านพันล้านและของเหล่านี้ "การจัดเรียง-and- วิเคราะห์" การดำเนินงานต่อวินาที ฉันเคยทำลำดับการจัดเรียงดีเอ็นเอที่ช่วยด้วย FPGA มาแล้วในอดีต แทบเป็นไปไม่ได้เลยที่จะเอาชนะพลังการประมวลผลที่ยิ่งใหญ่ของ FPGA เมื่อปัญหานั้นเหมาะสมกับโซลูชั่นประเภทนั้น

ในบางระดับปัจจัยที่ จำกัด เพียงอย่างเดียวจะทำให้คุณสามารถพลั่วข้อมูลลงใน FPGA ได้อย่างรวดเร็วและรวดเร็วเพียงใด

ในฐานะที่เป็นข้อมูลอ้างอิงฉันได้ออกแบบตัวประมวลผลภาพแบบเรียลไทม์ประสิทธิภาพสูงที่ได้รับข้อมูลภาพ RGB 32 บิตในอัตราประมาณ 300 ล้านพิกเซลต่อวินาที ข้อมูลที่ส่งผ่านตัวกรอง FIR ตัวคูณเมทริกซ์ตารางการค้นหาบล็อกการตรวจจับขอบเชิงพื้นที่และจำนวนของการดำเนินการอื่น ๆ ก่อนที่จะออกมาอีกด้านหนึ่ง ทั้งหมดนี้เป็น Xilinx Virtex2 FPGA ที่ค่อนข้างเล็กพร้อมการตอกบัตรภายในจากประมาณ 33MHz ไปจนถึงถ้าฉันจำได้อย่างถูกต้องก็คือ 400MHz โอ้ใช่แล้วมันยังมีตัวควบคุม DDR2 และใช้หน่วยความจำ DDR2 สองธนาคาร

FPGA สามารถส่งออกจำนวนสิบ 32 บิตเรียงลำดับทุกการเปลี่ยนนาฬิกาในขณะที่ทำงานที่หลายร้อย MHz จะมีความล่าช้าเล็กน้อยในช่วงเริ่มต้นของการดำเนินการเนื่องจากข้อมูลเติมไปป์ไลน์การประมวลผล / s หลังจากนั้นคุณควรจะได้รับผลลัพธ์หนึ่งรายการต่อนาฬิกา หรือมากกว่านั้นหากการประมวลผลนั้นสามารถทำให้ขนานได้โดยการจำลองเรซไปป์ไลน์และวิเคราะห์ โดยหลักการแล้ววิธีแก้ปัญหานั้นเป็นเรื่องเล็กน้อย

ประเด็นคือ: หากแอปพลิเคชันไม่ได้เชื่อมต่อกับพีซีและสตรีมข้อมูลและการประมวลผลนั้น "เข้ากันได้" กับโซลูชัน FPGA (ไม่ว่าจะเป็นแบบสแตนด์อโลนหรือเป็นการ์ดตัวประมวลผลร่วมในเครื่อง) ก็ไม่มีทางเป็นไปได้ เพื่อให้สามารถเอาชนะระดับประสิทธิภาพที่ทำได้ด้วยซอฟต์แวร์ที่เขียนในภาษาใด ๆ โดยไม่คำนึงถึงอัลกอริทึม

แก้ไข:

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

การเรียงลำดับเครือข่ายบน FPGA


10

ฉันเพิ่งเขียนคลาสเล็ก ๆที่ใช้อัลกอริทึม Bose-Nelson เพื่อสร้างเครือข่ายการเรียงลำดับในเวลารวบรวม

มันสามารถใช้เพื่อสร้างการเรียงลำดับที่รวดเร็วมากสำหรับ 10 หมายเลข

/**
 * A Functor class to create a sort for fixed sized arrays/containers with a
 * compile time generated Bose-Nelson sorting network.
 * \tparam NumElements  The number of elements in the array or container to sort.
 * \tparam T            The element type.
 * \tparam Compare      A comparator functor class that returns true if lhs < rhs.
 */
template <unsigned NumElements, class Compare = void> class StaticSort
{
    template <class A, class C> struct Swap
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            T t = Compare()(v0, v1) ? v0 : v1; // Min
            v1 = Compare()(v0, v1) ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A> struct Swap <A, void>
    {
        template <class T> inline void s(T &v0, T &v1)
        {
            // Explicitly code out the Min and Max to nudge the compiler
            // to generate branchless code.
            T t = v0 < v1 ? v0 : v1; // Min
            v1 = v0 < v1 ? v1 : v0; // Max
            v0 = t;
        }

        inline Swap(A &a, const int &i0, const int &i1) { s(a[i0], a[i1]); }
    };

    template <class A, class C, int I, int J, int X, int Y> struct PB
    {
        inline PB(A &a)
        {
            enum { L = X >> 1, M = (X & 1 ? Y : Y + 1) >> 1, IAddL = I + L, XSubL = X - L };
            PB<A, C, I, J, L, M> p0(a);
            PB<A, C, IAddL, J + M, XSubL, Y - M> p1(a);
            PB<A, C, IAddL, J, XSubL, M> p2(a);
        }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 1>
    {
        inline PB(A &a) { Swap<A, C> s(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 1, 2>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J); Swap<A, C> s1(a, I - 1, J - 1); }
    };

    template <class A, class C, int I, int J> struct PB <A, C, I, J, 2, 1>
    {
        inline PB(A &a) { Swap<A, C> s0(a, I - 1, J - 1); Swap<A, C> s1(a, I, J - 1); }
    };

    template <class A, class C, int I, int M, bool Stop = false> struct PS
    {
        inline PS(A &a)
        {
            enum { L = M >> 1, IAddL = I + L, MSubL = M - L};
            PS<A, C, I, L, (L <= 1)> ps0(a);
            PS<A, C, IAddL, MSubL, (MSubL <= 1)> ps1(a);
            PB<A, C, I, IAddL, L, MSubL> pb(a);
        }
    };

    template <class A, class C, int I, int M> struct PS <A, C, I, M, true>
    {
        inline PS(A &a) {}
    };

public:
    /**
     * Sorts the array/container arr.
     * \param  arr  The array/container to be sorted.
     */
    template <class Container> inline void operator() (Container &arr) const
    {
        PS<Container, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };

    /**
     * Sorts the array arr.
     * \param  arr  The array to be sorted.
     */
    template <class T> inline void operator() (T *arr) const
    {
        PS<T*, Compare, 1, NumElements, (NumElements <= 1)> ps(arr);
    };
};

#include <iostream>
#include <vector>

int main(int argc, const char * argv[])
{
    enum { NumValues = 10 };

    // Arrays
    {
        int rands[NumValues];
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    std::cout << "\n";

    // STL Vector
    {
        std::vector<int> rands(NumValues);
        for (int i = 0; i < NumValues; ++i) rands[i] = rand() % 100;
        std::cout << "Before Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
        StaticSort<NumValues> staticSort;
        staticSort(rands);
        std::cout << "After Sort: \t";
        for (int i = 0; i < NumValues; ++i) std::cout << rands[i] << " ";
        std::cout << "\n";
    }

    return 0;
}

โปรดทราบว่าแทนที่จะเป็นif (compare) swapคำสั่งเราได้เขียนโค้ดโอเปอร์เรเตอร์ที่ประกอบไปด้วย min และ max อย่างชัดเจน นี่คือการช่วยเขยิบคอมไพเลอร์ให้ใช้รหัส branchless

มาตรฐาน

มาตรฐานต่อไปนี้ได้รับการรวบรวมด้วยเสียงดังกราว -O3 และวิ่งบนอากาศ macbook กลางปี ​​2012 ของฉัน

เรียงลำดับข้อมูลแบบสุ่ม

เปรียบเทียบกับรหัสของ DarioP นี่คือจำนวนมิลลิวินาทีที่ใช้ในการเรียงลำดับ 1 ล้าน 32- บิต int อาร์เรย์ขนาด 10:

Hardcoded Sort Net 10: 88.774 ms
Templated Bose-Nelson เรียงลำดับ 10: 27.815 ms

ใช้วิธีการ templated นี้เรายังสามารถสร้างเครือข่ายการเรียงลำดับตามเวลารวบรวมสำหรับองค์ประกอบอื่น ๆ

เวลา (เป็นมิลลิวินาที) เพื่อเรียง 1 ล้านอาร์เรย์ของขนาดต่างๆ
จำนวนมิลลิวินาทีสำหรับอาร์เรย์ขนาด 2, 4, 8 คือ 1.943, 8.655, 20.246 ตามลำดับ
C ++ Templated การกำหนดเวลาการจัดเรียงแบบคงที่ของ Bose-Nelson

มอบเครดิตให้กับGlenn Teitelbaumสำหรับการเรียงลำดับการแทรกที่ยังไม่ได้ควบคุม

นี่คือนาฬิกาเฉลี่ยต่อการเรียงลำดับสำหรับอาร์เรย์ขนาดเล็ก 6 องค์ประกอบ รหัสมาตรฐานและตัวอย่างสามารถพบได้ในคำถามนี้:
เรียงลำดับที่เร็วที่สุดของ 6 int อาร์เรย์คงที่

Direct call to qsort library function       : 326.81
Naive implementation (insertion sort)       : 132.98
Insertion Sort (Daniel Stutzbach)           : 104.04
Insertion Sort Unrolled                     : 99.64
Insertion Sort Unrolled (Glenn Teitelbaum)  : 81.55
Rank Order                                  : 44.01
Rank Order with registers                   : 42.40
Sorting Networks (Daniel Stutzbach)         : 88.06
Sorting Networks (Paul R)                   : 31.64
Sorting Networks 12 with Fast Swap          : 29.68
Sorting Networks 12 reordered Swap          : 28.61
Reordered Sorting Network w/ fast swap      : 24.63
Templated Sorting Network (this class)      : 25.37

มันทำงานได้เร็วเท่ากับตัวอย่างที่เร็วที่สุดในคำถามสำหรับ 6 องค์ประกอบ

ประสิทธิภาพสำหรับการเรียงลำดับข้อมูลที่เรียงลำดับ

บ่อยครั้งที่อินพุตแถวเรียงอาจถูกเรียงลำดับแล้วหรือเรียงเป็นส่วนใหญ่
ในกรณีเช่นนี้การเรียงลำดับการแทรกอาจเป็นทางเลือกที่ดีกว่า

ป้อนคำอธิบายรูปภาพที่นี่

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

รหัสที่ใช้สำหรับมาตรฐานที่สามารถพบได้ที่นี่


โอกาสใดที่คุณสามารถเพิ่มการเปรียบเทียบสำหรับอัลโกของฉันด้านล่าง?
Glenn Teitelbaum

@GlennTeitelbaum โอกาสใด ๆ ที่คุณเพิ่มรายการนี้ลงของมาตรฐานและวิธีการเปิดเผยและผล?
greybeard

ความรุ่งโรจน์สำหรับการเพิ่มข้อมูลเกี่ยวกับการเรียงลำดับการป้อนข้อมูลที่จัดเรียง
greybeard

ในบางระบบv1 = v0 < v1 ? v1 : v0; // Maxอาจจะยังสาขาในกรณีที่ว่ามันสามารถถูกแทนที่ด้วยv1 += v0 - tเพราะถ้าtเป็นv0แล้วv1 + v0 -t == v1 + v0 - v0 == v1อื่นtเป็นv1และv1 + v0 -t == v1 + v0 - v1 == v0
เกล็น Teitelbaum

ไตรภาคีมักจะรวบรวมเป็นmaxssหรือการminssเรียนการสอนในคอมไพเลอร์ที่ทันสมัย แต่ในกรณีที่ใช้งานไม่ได้สามารถใช้วิธีการแลกเปลี่ยนอื่นได้ :)
Vectorized

5

แม้ว่าการจัดเรียงเครือข่ายจะมีอัตราต่อรองที่ดีว่ารวดเร็วในอาร์เรย์ขนาดเล็ก แต่บางครั้งคุณไม่สามารถเอาชนะการเรียงลำดับการแทรกได้หากปรับให้เหมาะสม ตัวอย่างเช่นการแทรกแบทช์ที่มี 2 องค์ประกอบ:

{
    final int a=in[0]<in[1]?in[0]:in[1];
    final int b=in[0]<in[1]?in[1]:in[0];
    in[0]=a;
    in[1]=b;
}
for(int x=2;x<10;x+=2)
{
    final int a=in[x]<in[x+1]?in[x]:in[x+1];
    final int b=in[x]<in[x+1]?in[x+1]:in[x];
    int y= x-1;

    while(y>=0&&in[y]>b)
    {
        in[y+2]= in[y];
        --y;
    }
    in[y+2]=b;
    while(y>=0&&in[y]>a)
    {
        in[y+1]= in[y];
        --y;
    }
    in[y+1]=a;
}

ไม่แน่ใจว่าทำไมคุณถึงทำซ้ำin[y+2]= in[y];พิมพ์ผิด?
Glenn Teitelbaum

ว้าวฉันจะทำยังไง และใครบางคนใช้เวลานานแค่ไหนที่จะสังเกตเห็น? คำตอบ: ไม่ใช่การพิมพ์ผิด: ฉันกำลังปรับเปลี่ยนอัลกอริทึมที่แตกต่างกันซึ่งมีทั้งคีย์และอาร์เรย์ค่า
วอร์เรน

3

คุณสามารถคลี่คลายได้อย่างสมบูรณ์ insertion sort

เพื่อให้ง่ายขึ้นtemplateคุณสามารถใช้ s แบบเรียกซ้ำโดยไม่มีฟังก์ชั่นโอเวอร์เฮด เนื่องจากมันมีอยู่แล้วเป็นtemplate, intอาจจะเป็นtemplateพารามิเตอร์เช่นกัน นอกจากนี้ยังทำให้การเข้ารหัสขนาดอาร์เรย์อื่น ๆ นอกเหนือจาก 10 เล็กน้อยเพื่อสร้าง

โปรดทราบว่าการเรียงลำดับint x[10]การโทรนั้นเกิดขึ้นinsert_sort<int, 9>::sort(x);เนื่องจากคลาสใช้ดัชนีของรายการสุดท้าย สิ่งนี้อาจถูกห่อไว้ แต่นั่นจะเป็นรหัสที่มากกว่าให้อ่าน

template <class T, int NUM>
class insert_sort;

template <class T>
class insert_sort<T,0>
// stop template recursion
// sorting 1 item is a no-op
{
public:
    static void place(T *x) {}
    static void sort(T * x) {}
};

template <class T, int NUM>
class insert_sort
// use template recursion to do insertion sort
// NUM is the index of the last item, eg. for x[10] call <9>
{
public:
    static void place(T *x)
    {
        T t1=x[NUM-1];
        T t2=x[NUM];
        if (t1 > t2)
        {
            x[NUM-1]=t2;
            x[NUM]=t1;
            insert_sort<T,NUM-1>::place(x);
        }
    }
    static void sort(T * x)
    {
        insert_sort<T,NUM-1>::sort(x); // sort everything before
        place(x);                    // put this item in
    }
};

ในการทดสอบของฉันนี้เร็วกว่าตัวอย่างเครือข่ายการเรียงลำดับ


0

สำหรับเหตุผลที่คล้ายกับที่ฉันอธิบายไว้ที่นี่ฟังก์ชันการเรียงลำดับต่อไปนี้sort6_iterator()และsort10_iterator_local()ควรทำงานได้ดีโดยที่เครือข่ายการเรียงลำดับถูกนำมาจากที่นี่ :

template<class IterType> 
inline void sort10_iterator(IterType it) 
{
#define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);}
#define DD1(a)   auto data##a=*(data+a);
#define DD2(a,b) auto data##a=*(data+a), data##b=*(data+b);
#define CB1(a)   *(data+a)=data##a;
#define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b;
  DD2(1,4) SORT2(1,4) DD2(7,8) SORT2(7,8) DD2(2,3) SORT2(2,3) DD2(5,6) SORT2(5,6) DD2(0,9) SORT2(0,9) 
  SORT2(2,5) SORT2(0,7) SORT2(8,9) SORT2(3,6) 
  SORT2(4,9) SORT2(0,1) 
  SORT2(0,2) CB1(0) SORT2(6,9) CB1(9) SORT2(3,5) SORT2(4,7) SORT2(1,8) 
  SORT2(3,4) SORT2(5,8) SORT2(6,7) SORT2(1,2) 
  SORT2(7,8) CB1(8) SORT2(1,3) CB1(1) SORT2(2,5) SORT2(4,6) 
  SORT2(2,3) CB1(2) SORT2(6,7) CB1(7) SORT2(4,5) 
  SORT2(3,4) CB2(3,4) SORT2(5,6) CB2(5,6) 
#undef CB1
#undef CB2
#undef DD1
#undef DD2
#undef SORT2
}

ในการเรียกใช้ฟังก์ชั่นนี้ฉันผ่านมันเป็นตัวstd::vectorวนซ้ำ


0

การเรียงลำดับการแทรกต้องการโดยเฉลี่ย 29,6 การเปรียบเทียบเพื่อเรียงลำดับ 10 อินพุตด้วยกรณีที่ดีที่สุดของ 9 และเลวร้ายที่สุดของ 45 (อินพุตที่กำหนดซึ่งอยู่ในลำดับย้อนกลับ)

{9,6,1} shellsort จะต้องใช้การเปรียบเทียบ 25.5 โดยเฉลี่ยเพื่อเรียงลำดับ 10 อินพุต กรณีที่ดีที่สุดคือ 14 การเปรียบเทียบที่เลวร้ายที่สุดคือ 34 และการเรียงลำดับข้อมูลที่ตรงกันข้ามต้องใช้ 22

ดังนั้นการใช้ shellsort แทนการเรียงลำดับการแทรกจะลดขนาดตัวพิมพ์เฉลี่ยลง 14% แม้ว่าเคสที่ดีที่สุดจะเพิ่มขึ้น 56% เคสที่แย่ที่สุดจะลดลง 24% ซึ่งมีความสำคัญในแอปพลิเคชันซึ่งการรักษาประสิทธิภาพของเคสที่แย่ที่สุดในการตรวจสอบเป็นสิ่งสำคัญ เคสกลับจะลดลง 51%

เนื่องจากคุณคุ้นเคยกับการเรียงลำดับการแทรกคุณสามารถใช้อัลกอริทึมเป็นเครือข่ายการเรียงลำดับสำหรับ {9,6} แล้วทำการเรียงลำดับการแทรก ({1}) หลังจากนั้น:

i[0] with i[9]    // {9}

i[0] with i[6]    // {6}
i[1] with i[7]    // {6}
i[2] with i[8]    // {6}
i[3] with i[9]    // {6}

i[0 ... 9]        // insertion sort
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.