การแทนที่ตัวนับลูป 32 บิตเป็น 64 บิตจะนำเสนอการเบี่ยงเบนประสิทธิภาพที่บ้าคลั่งด้วย _mm_popcnt_u64 บน Intel CPUs


1424

ฉันกำลังมองหาวิธีที่เร็วที่สุดในการจัดpopcountเก็บข้อมูลขนาดใหญ่ ฉันพบลักษณะพิเศษที่แปลกมาก : การเปลี่ยนตัวแปรลูปจากunsignedเป็นuint64_tทำให้ประสิทธิภาพลดลง 50% บนพีซีของฉัน

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

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

    using namespace std;
    if (argc != 2) {
       cerr << "usage: array_size in MB" << endl;
       return -1;
    }

    uint64_t size = atol(argv[1])<<20;
    uint64_t* buffer = new uint64_t[size/8];
    char* charbuffer = reinterpret_cast<char*>(buffer);
    for (unsigned i=0; i<size; ++i)
        charbuffer[i] = rand()%256;

    uint64_t count,duration;
    chrono::time_point<chrono::system_clock> startP,endP;
    {
        startP = chrono::system_clock::now();
        count = 0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with unsigned
            for (unsigned i=0; i<size/8; i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }
    {
        startP = chrono::system_clock::now();
        count=0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with uint64_t
            for (uint64_t i=0;i<size/8;i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }

    free(charbuffer);
}

อย่างที่คุณเห็นเราสร้างบัฟเฟอร์ของข้อมูลสุ่มโดยมีขนาดเป็นxเมกะไบต์ซึ่งxอ่านได้จากบรรทัดคำสั่ง หลังจากนั้นเราวนซ้ำบัฟเฟอร์และใช้ x86 รุ่นที่ไม่ได้ควบคุมpopcountเพื่อดำเนินการกับ popcount เพื่อให้ได้ผลลัพธ์ที่แม่นยำยิ่งขึ้นเราได้ทำ popcount 10,000 ครั้ง เราวัดเวลาสำหรับ popcount ในกรณีที่บนตัวแปรภายในวงเป็นในกรณีที่ต่ำกว่าตัวแปรภายในวงคือunsigned uint64_tฉันคิดว่าสิ่งนี้ไม่ควรสร้างความแตกต่าง แต่อย่างตรงกันข้าม

ผลลัพธ์ (บ้าสุด ๆ )

ฉันรวบรวมมันเช่นนี้ (รุ่น g ++: Ubuntu 4.8.2-19ubuntu1):

g++ -O3 -march=native -std=c++11 test.cpp -o test

นี่คือผลลัพธ์บนHaswell Core i7-4770K CPU ของฉันที่3.50 GHz ที่ใช้งานtest 1(ดังนั้นข้อมูลสุ่ม 1 MB):

  • ไม่ได้ลงนาม 41959360000 0.401554 วินาที 26.113 GB / s
  • uint64_t 41959360000 0.759822 วินาที 13.8003 GB / s

ตามที่คุณเห็นปริมาณงานของuint64_tรุ่นนี้เป็นเพียงครึ่งหนึ่งของunsignedรุ่น! ปัญหาดูเหมือนว่าจะเกิดการชุมนุมที่แตกต่างกัน แต่ทำไม? ครั้งแรกฉันคิดว่าข้อผิดพลาดของคอมไพเลอร์ดังนั้นฉันจึงลองclang++(Ubuntu Clangเวอร์ชั่น 3.4-1ubuntu3):

clang++ -O3 -march=native -std=c++11 teest.cpp -o test

ผลลัพธ์: test 1

  • ไม่ได้ลงนาม 41959360000 0.398293 วินาที 26.3267 GB / s
  • uint64_t 41959360000 0.680954 วินาที 15.3986 GB / s

ดังนั้นมันเกือบจะเป็นผลลัพธ์เดียวกันและก็ยังแปลก แต่ตอนนี้มันแปลกมาก ฉันแทนที่ขนาดบัฟเฟอร์ที่อ่านจากอินพุตด้วยค่าคงที่1ดังนั้นฉันจึงเปลี่ยน:

uint64_t size = atol(argv[1]) << 20;

ถึง

uint64_t size = 1 << 20;

ดังนั้นคอมไพเลอร์จึงรู้ขนาดบัฟเฟอร์ในเวลารวบรวม บางทีมันอาจเพิ่มการเพิ่มประสิทธิภาพบางอย่าง! นี่คือตัวเลขสำหรับg++:

  • ไม่ได้ลงนาม 41959360000 0.509156 วินาที 20.5944 GB / s
  • uint64_t 41959360000 0.508673 วินาที 20.6139 GB / s

ตอนนี้ทั้งสองรุ่นเร็วพอ ๆ กัน อย่างไรก็ตามการunsigned ได้ช้าลง ! มันลดลงจาก26การ20 GB/sจึงเปลี่ยนไม่ใช่อย่างต่อเนื่องโดยนำค่าคงที่ไปdeoptimization อย่างจริงจังฉันไม่มีเงื่อนงำสิ่งที่เกิดขึ้นที่นี่! แต่ตอนนี้ไปclang++กับรุ่นใหม่:

  • ไม่ได้ลงนาม 41959360000 0.677009 วินาที 15.4884 GB / s
  • uint64_t 41959360000 0.676909 วินาที 15.4906 GB / s

รออะไร? ตอนนี้ทั้งสองเวอร์ชันลดลงเป็นจำนวนช้า 15 GB / s ดังนั้นการแทนที่ค่าคงที่ด้วยค่าคงที่จะทำให้โค้ดช้าลงในทั้งสองกรณีสำหรับ Clang!

ฉันถามเพื่อนร่วมงานกับIvy Bridge CPU เพื่อรวบรวมมาตรฐานของฉัน เขาได้ผลลัพธ์ที่คล้ายกันดังนั้นจึงดูเหมือนจะไม่มีแฮส เนื่องจากคอมไพเลอร์สองตัวสร้างผลลัพธ์แปลก ๆ ที่นี่จึงดูเหมือนจะไม่เป็นข้อผิดพลาดของคอมไพเลอร์ เราไม่มี CPU ของ AMD ที่นี่ดังนั้นเราสามารถทดสอบกับ Intel เท่านั้น

บ้ามากขึ้นได้โปรด!

ใช้ตัวอย่างแรก (อันที่มีatol(argv[1])) และวางไว้ข้างstaticหน้าตัวแปรเช่น:

static uint64_t size=atol(argv[1])<<20;

นี่คือผลลัพธ์ของฉันใน g ++:

  • ไม่ได้ลงนาม 41959360000 0.396728 วินาที 26.4306 GB / s
  • uint64_t 41959360000 0.509484 วินาที 20.5811 GB / s

ยายยังอีกทางเลือกหนึ่ง เรายังมีความเร็ว 26 GB / s ด้วยu32แต่เราจัดการเพื่อให้ได้u64อย่างน้อยจาก 13 GB / s ไปเป็นรุ่น 20 GB / s! บนพีซีของเพื่อนร่วมงานของฉันu64เวอร์ชันกลายเป็นเร็วกว่าu32เวอร์ชันทำให้ได้ผลลัพธ์ที่เร็วที่สุด น่าเศร้านี้ทำงานเฉพาะสำหรับg++, ดูเหมือนจะไม่เกี่ยวกับการดูแลclang++static

คำถามของฉัน

คุณอธิบายผลลัพธ์เหล่านี้ได้ไหม โดยเฉพาะอย่างยิ่ง:

  • จะมีความแตกต่างระหว่างu32และu64อย่างไร
  • วิธีสามารถแทนที่ non-constant ด้วยขนาดบัฟเฟอร์คงที่ทำให้รหัสที่เหมาะสมน้อยลงได้อย่างไร
  • การแทรกstaticคำหลักจะทำให้u64วนซ้ำเร็วขึ้นได้อย่างไร ยิ่งเร็วกว่ารหัสต้นฉบับในคอมพิวเตอร์ของเพื่อนร่วมงานของฉัน!

ฉันรู้ว่าการปรับให้เหมาะสมนั้นเป็นดินแดนที่มีเล่ห์เหลี่ยม แต่ฉันไม่เคยคิดเลยว่าการเปลี่ยนแปลงเล็ก ๆ เหล่านี้สามารถนำไปสู่ความแตกต่างในเวลาดำเนินการ100%และปัจจัยเล็ก ๆ เช่นขนาดบัฟเฟอร์คงที่สามารถผสมผลลัพธ์ทั้งหมดอีกครั้ง แน่นอนฉันต้องการมีรุ่นที่สามารถ popcount 26 GB / s ได้เสมอ วิธีเดียวที่เชื่อถือได้ที่ฉันคิดได้คือคัดลอกวางชุดประกอบสำหรับกรณีนี้และใช้ชุดประกอบแบบอินไลน์ นี่เป็นวิธีเดียวที่ฉันสามารถกำจัดคอมไพเลอร์ที่ดูเหมือนจะคลั่งไคล้กับการเปลี่ยนแปลงเล็กน้อย คุณคิดอย่างไร? มีวิธีอื่นในการรับรหัสที่มีประสิทธิภาพสูงสุดอย่างน่าเชื่อถือหรือไม่

การถอดประกอบ

นี่คือการถอดชิ้นส่วนสำหรับผลลัพธ์ต่าง ๆ :

รุ่น 26 GB / s จากg ++ / u32 / ขนาดที่ไม่คงที่ :

0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8

รุ่น 13 GB / s จากg ++ / u64 / ขนาดที่ไม่คงที่ :

0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00

รุ่น 15 GB / s จากclang ++ / u64 / bufsize ที่ไม่ใช่ const :

0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50

รุ่น 20 GB / s จากg ++ / u32 & u64 / const bufsize :

0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68

รุ่น 15 GB / s จากclang ++ / u32 & u64 / const bufsize :

0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0

น่าสนใจรุ่นที่เร็วที่สุด (26 GB / s) ก็ยาวที่สุดเช่นกัน! leaมันน่าจะเป็นเพียงการแก้ปัญหาที่ใช้ บางรุ่นใช้ที่จะข้ามไปที่คนอื่นใช้jb jneแต่นอกเหนือจากนั้นทุกรุ่นดูเหมือนจะเทียบเคียง ฉันไม่เห็นว่าช่องว่างของประสิทธิภาพ 100% นั้นมาจากไหน แต่ฉันไม่เชี่ยวชาญในการถอดรหัสการชุมนุม รุ่นที่ช้าที่สุด (13 GB / s) จะดูสั้นและดีมาก มีใครอธิบายเรื่องนี้ได้บ้าง

บทเรียนที่ได้เรียนรู้

ไม่ว่าคำตอบสำหรับคำถามนี้จะเป็นอย่างไร ฉันได้เรียนรู้ว่าในลูปร้อนจริงๆทุกรายละเอียดสามารถเรื่องรายละเอียดแม้กระทั่งที่ไม่ได้ดูเหมือนจะมีการเชื่อมโยงไปยังรหัสร้อนใดฉันไม่เคยคิดเกี่ยวกับประเภทของการใช้สำหรับตัวแปรลูป แต่อย่างที่คุณเห็นการเปลี่ยนแปลงเล็กน้อยสามารถสร้างความแตกต่างได้100% ! แม้แต่ประเภทการจัดเก็บของบัฟเฟอร์ก็สามารถสร้างความแตกต่างได้อย่างมากดังที่เราเห็นด้วยการแทรกstaticคำหลักไว้ด้านหน้าตัวแปรขนาด! ในอนาคตฉันจะทดสอบทางเลือกที่หลากหลายในคอมไพเลอร์ต่าง ๆ เสมอเมื่อเขียนลูปที่แน่นและร้อนแรงซึ่งมีความสำคัญต่อประสิทธิภาพของระบบ

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


8
ความคิดเห็นมากมาย! คุณสามารถดูพวกเขาในการแชทและแม้กระทั่งปล่อยให้เป็นของคุณเองหากคุณต้องการ แต่โปรดอย่าเพิ่มที่นี่อีก!
Shog9

3
ยังเห็นGCC ฉบับที่ 62011, การอ้างอิงข้อมูลเท็จในการเรียนการสอน มีคนอื่นจัดหา แต่ดูเหมือนว่าจะหายไประหว่างการสะสาง
jww

ฉันไม่สามารถบอกได้ว่าเป็นชิ้นส่วนแยกต่างหากสำหรับรุ่นที่มีสแตติกหรือไม่ ถ้าไม่คุณสามารถแก้ไขโพสต์และเพิ่มได้หรือไม่
Kelly S. French

คำตอบ:


1552

ผู้ร้าย: การพึ่งพาข้อมูลเท็จ (และคอมไพเลอร์ไม่ได้ตระหนักถึงมัน)

บนตัวประมวลผล Sandy / Ivy Bridge และ Haswell คำสั่ง:

popcnt  src, dest

destดูเหมือนจะมีการพึ่งพาที่ผิดพลาดในการลงทะเบียนปลายทาง แม้ว่าคำสั่งจะเขียนไปยังคำสั่งนั้นเท่านั้นคำสั่งจะรอจนกว่าdestจะพร้อมก่อนดำเนินการ การอ้างอิงที่ผิดพลาดนี้ (ตอนนี้) บันทึกโดย Intel ว่า erratum HSD146 (Haswell)และSKL029 (Skylake)

Skylake คงที่นี้lzcnttzcntและ
แคนนอน Lake (และทะเลสาบน้ำแข็ง) popcntคงที่นี้
bsf/ bsrมีการพึ่งพาเอาต์พุตที่แท้จริง: เอาต์พุตที่ไม่ได้แก้ไขสำหรับอินพุต = 0 (แต่ไม่มีวิธีที่จะใช้ประโยชน์จากสิ่งนั้นด้วยอินทิลิตี้ - มีเพียงเอกสารของเอเอ็มดีเท่านั้นและคอมไพเลอร์จะไม่เปิดเผย)

(ใช่คำแนะนำเหล่านี้ทั้งหมดทำงานในหน่วยดำเนินการเดียวกัน )


การขึ้นต่อกันนี้ไม่ได้เป็นเพียงแค่ยก 4 popcnts จากการวนซ้ำแบบวนซ้ำ สามารถดำเนินการข้ามการวนซ้ำทำให้โปรเซสเซอร์ไม่สามารถทำการวนซ้ำแบบวนซ้ำได้

การปรับแต่งunsignedvs. uint64_tและอื่น ๆ ไม่ส่งผลกระทบต่อปัญหาโดยตรง แต่จะมีผลต่อตัวจัดสรรการลงทะเบียนซึ่งกำหนดค่าการลงทะเบียนให้กับตัวแปร

ในกรณีของคุณความเร็วเป็นผลโดยตรงของสิ่งที่ติดอยู่กับห่วงโซ่การพึ่งพา (false) ขึ้นอยู่กับสิ่งที่ตัวจัดสรรการลงทะเบียนตัดสินใจทำ

  • 13 GB / s มีโซ่: popcnt- add- popcnt- popcnt→การวนซ้ำถัดไป
  • 15 GB / s มีโซ่: popcnt- add- popcnt- add→การวนซ้ำถัดไป
  • 20 GB / s มีโซ่: popcnt- popcnt→การวนซ้ำถัดไป
  • 26 GB / s มีโซ่: popcnt- popcnt→การทำซ้ำต่อไป

ความแตกต่างระหว่าง 20 GB / s และ 26 GB / s ดูเหมือนจะเป็นสิ่งประดิษฐ์เล็กน้อยของการกำหนดที่อยู่ทางอ้อม ตัวประมวลผลเริ่มต้นที่จะตีคอขวดอื่น ๆ เมื่อคุณมาถึงความเร็วนี้


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

นี่คือผลลัพธ์:

Sandy Bridge Xeon @ 3.5 GHz: (รหัสทดสอบเต็มสามารถพบได้ที่ด้านล่าง)

  • GCC 4.6.3: g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native
  • Ubuntu 12

รีจิสเตอร์ที่แตกต่างกัน: 18.6195 GB / s

.L4:
    movq    (%rbx,%rax,8), %r8
    movq    8(%rbx,%rax,8), %r9
    movq    16(%rbx,%rax,8), %r10
    movq    24(%rbx,%rax,8), %r11
    addq    $4, %rax

    popcnt %r8, %r8
    add    %r8, %rdx
    popcnt %r9, %r9
    add    %r9, %rcx
    popcnt %r10, %r10
    add    %r10, %rdi
    popcnt %r11, %r11
    add    %r11, %rsi

    cmpq    $131072, %rax
    jne .L4

ลงทะเบียนแบบเดียวกัน: 8.49272 GB / s

.L9:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # This time reuse "rax" for all the popcnts.
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L9

ลงทะเบียนเหมือนกันกับลูกโซ่ที่ขาด: 17.8869 GB / s

.L14:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # Reuse "rax" for all the popcnts.
    xor    %rax, %rax    # Break the cross-iteration dependency by zeroing "rax".
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L14

แล้วมีอะไรผิดพลาดกับคอมไพเลอร์?

ดูเหมือนว่าทั้ง GCC และ Visual Studio ไม่ทราบว่าpopcntมีการอ้างอิงที่ผิดดังกล่าว อย่างไรก็ตามการอ้างอิงเท็จเหล่านี้ไม่ใช่เรื่องแปลก เป็นเพียงเรื่องของคอมไพเลอร์ที่ตระหนักถึงมันหรือไม่

popcntไม่ใช่คำสั่งที่ใช้กันมากที่สุด ดังนั้นจึงไม่น่าแปลกใจจริงๆที่ผู้รวบรวมรายใหญ่อาจพลาดบางสิ่งเช่นนี้ ดูเหมือนว่าจะไม่มีเอกสารใด ๆ ที่กล่าวถึงปัญหานี้ หาก Intel ไม่เปิดเผยจะไม่มีใครรู้ข้างนอกจนกว่าจะมีคนวิ่งเข้ามาโดยบังเอิญ

( อัปเดต: ตั้งแต่เวอร์ชัน 4.9.2 , GCC ทราบถึงการพึ่งพาเท็จและสร้างรหัสเพื่อชดเชยเมื่อเปิดใช้งานการปรับให้เหมาะสมคอมไพเลอร์หลักจากผู้จำหน่ายรายอื่นรวมถึง Clang, MSVC และแม้แต่ ICC ของ Intel เองยังไม่ทราบ errarum microarchitectural นี้และจะไม่ปล่อยโค้ดที่ชดเชยให้)

ทำไมซีพียูถึงมีการอ้างอิงผิด ๆ

เราสามารถคาดการณ์: จะทำงานบนหน่วยดำเนินการเช่นเดียวกับbsf/ bsrที่ทำมีการพึ่งพาการส่งออก ( POPCNT นำไปใช้ในฮาร์ดแวร์ได้อย่างไร ) สำหรับคำแนะนำเหล่านั้น Intel เอกสารผลลัพธ์จำนวนเต็มสำหรับอินพุต = 0 เป็น "undefined" (พร้อม ZF = 1) แต่ฮาร์ดแวร์ของ Intel ให้การรับประกันที่แข็งแกร่งกว่าเพื่อหลีกเลี่ยงการทำลายซอฟต์แวร์เก่า: เอาท์พุทที่ไม่ได้แก้ไข AMD บันทึกพฤติกรรมนี้

สันนิษฐานว่าเป็นเรื่องไม่สะดวกที่จะทำให้ uops บางตัวสำหรับยูนิตการดำเนินการนี้ขึ้นอยู่กับเอาต์พุต

โปรเซสเซอร์ AMD ไม่ปรากฏว่ามีการอ้างอิงที่ผิดพลาดนี้


รหัสทดสอบเต็มรูปแบบด้านล่างสำหรับการอ้างอิง:

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

   using namespace std;
   uint64_t size=1<<20;

   uint64_t* buffer = new uint64_t[size/8];
   char* charbuffer=reinterpret_cast<char*>(buffer);
   for (unsigned i=0;i<size;++i) charbuffer[i]=rand()%256;

   uint64_t count,duration;
   chrono::time_point<chrono::system_clock> startP,endP;
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %4  \n\t"
                "add %4, %0     \n\t"
                "popcnt %5, %5  \n\t"
                "add %5, %1     \n\t"
                "popcnt %6, %6  \n\t"
                "add %6, %2     \n\t"
                "popcnt %7, %7  \n\t"
                "add %7, %3     \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "No Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Chain 4   \t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "xor %%rax, %%rax   \n\t"   // <--- Break the chain.
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Broken Chain\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }

   free(charbuffer);
}

มาตรฐานที่น่าสนใจเท่าเทียมกันสามารถพบได้ที่นี่: http://pastebin.com/kbzgL8si
มาตรฐานนี้แตกต่างกันไปจำนวนของpopcntที่อยู่ในห่วงโซ่การพึ่งพา (เท็จ)

False Chain 0:  41959360000 0.57748 sec     18.1578 GB/s
False Chain 1:  41959360000 0.585398 sec    17.9122 GB/s
False Chain 2:  41959360000 0.645483 sec    16.2448 GB/s
False Chain 3:  41959360000 0.929718 sec    11.2784 GB/s
False Chain 4:  41959360000 1.23572 sec     8.48557 GB/s

3
สวัสดีทุกคน! ความคิดเห็นที่ผ่านมามากมายที่นี่; ก่อนที่จะออกใหม่โปรดตรวจสอบข้อมูลที่เก็บ
Shog9

1
@ JustinL.it ดูเหมือนว่าปัญหานี้ได้รับการแก้ไขใน Clang ตั้งแต่วันที่ 7.0
Dan M.

@PeterCordes ฉันไม่คิดว่ามันเป็นหน่วยดำเนินการเท่าที่เป็นตัวกำหนดตารางเวลา มันเป็นตัวกำหนดตารางเวลาที่ติดตามการพึ่งพา และในการทำเช่นนั้นคำสั่งจะถูกจัดกลุ่มเป็นจำนวน "คลาสคำสั่ง" ซึ่งแต่ละตัวจะได้รับการปฏิบัติเหมือนกันโดยตัวกำหนดตารางเวลา ดังนั้นคำสั่ง 3 รอบ "slow-int" จึงถูกโยนลงใน "คลาส" เดียวกันเพื่อวัตถุประสงค์ในการจัดตารางการเรียนการสอน
Mysticial

@Mysticial: คุณยังคิดอย่างนั้นเหรอ? เป็นไปได้ แต่ imul dst, src, immไม่ได้มีการขึ้นต่อกันของผลผลิตและไม่ช้าleaเลย ไม่เช่นpdepนั้น แต่นั่นคือ VEX ที่เข้ารหัสด้วย 2 ตัวถูกดำเนินการอินพุต ตกลงไม่ใช่หน่วยดำเนินการเองที่ทำให้เกิด dep ที่ผิดพลาด ที่ขึ้นอยู่กับ RAT และออก / เปลี่ยนชื่อเวทีในขณะที่มันเปลี่ยนชื่อตัวถูกดำเนินการลงทะเบียนสถาปัตยกรรมลงทะเบียนทางกายภาพ สันนิษฐานว่ามันต้องการตารางของ uop-code -> รูปแบบการพึ่งพาและตัวเลือกพอร์ตและการจัดกลุ่ม uops ทั้งหมดสำหรับหน่วยการดำเนินการเดียวกันร่วมกันทำให้ตารางนั้นง่ายขึ้น นั่นคือสิ่งที่ฉันต้องการในรายละเอียดเพิ่มเติม
ปีเตอร์

แจ้งให้เราทราบหากคุณต้องการให้ฉันแก้ไขในคำตอบของคุณหรือถ้าคุณต้องการนำกลับไปพูดสิ่งที่คุณพูดเกี่ยวกับกำหนดการ ความจริงที่ว่า SKL ลดระดับ false สำหรับ lzcnt / tzcnt แต่ไม่ใช่ popcnt ควรบอกบางอย่างกับเรา แต่ IDK คืออะไร อีกสัญญาณหนึ่งที่เป็นไปได้ว่าจะเปลี่ยนชื่อ / เกี่ยวข้องกับ RAT นั่นคือ SKL จะลบโหมดการกำหนดแอดเดรสที่จัดทำดัชนีเป็นแหล่งหน่วยความจำสำหรับ lzcnt / tzcnt แต่ไม่ใช่ popcnt เห็นได้ชัดว่าหน่วยการเปลี่ยนชื่อจะต้องสร้าง uops back-end สามารถเป็นตัวแทนแม้ว่า
ปีเตอร์

50

ฉันเขียนโปรแกรม C ที่เทียบเท่าเพื่อทำการทดลองและฉันสามารถยืนยันพฤติกรรมที่แปลกประหลาดนี้ได้ ยิ่งไปกว่านั้นgccเชื่อว่าจำนวนเต็ม 64- บิต (ซึ่งน่าจะเป็นsize_tต่อไป ... ) จะดีกว่าเนื่องจากการใช้uint_fast32_tทำให้ gcc ใช้ 64- บิต uint

ฉันทำบิตของการล้อเล่นรอบด้วยการชุมนุม:
เพียงแค่ใช้รุ่น 32 บิตแทนที่คำแนะนำ / การลงทะเบียน 32 บิตทั้งหมดด้วยรุ่น 64 บิตใน popcount-loop ภายในของโปรแกรม การสังเกต: โค้ดเร็วพอ ๆ กับเวอร์ชั่น 32 บิต! จากนั้นฉันก็คัดลอกโค้ดวนรอบด้านในจากโปรแกรมรุ่น 32 บิตแฮ็คมันเป็น 64 บิตเล่นกับรีจิสเตอร์เพื่อแทนที่วงวนภายในของเวอร์ชัน 64 บิต รหัสนี้ยังทำงานเร็วเท่ากับรุ่น 32 บิต

เห็นได้ชัดว่านี่คือการแฮ็กเนื่องจากขนาดของตัวแปรไม่ได้เป็น 64 บิตเนื่องจากส่วนอื่น ๆ ของโปรแกรมยังคงใช้รุ่น 32 บิต แต่ตราบใดที่ popcount-loop นั้นมีประสิทธิภาพดีกว่านี่เป็นการเริ่มต้นที่ดี .



ข้อสรุปของฉันคือว่านี่คือการตั้งเวลาคำสั่งที่ไม่ถูกต้องโดยคอมไพเลอร์ไม่ใช่ข้อได้เปรียบความเร็ว / แฝงจริงของคำแนะนำแบบ 32 บิต

(Caveat: ฉันแฮ็กอัพการชุมนุมอาจทำให้บางสิ่งบางอย่างเสียหายโดยไม่สังเกตเห็นฉันไม่คิดอย่างนั้น)


1
“ ยิ่งไปกว่านั้น gcc เชื่อว่าจำนวนเต็ม 64- บิต […] จะดีกว่าเนื่องจากการใช้ uint_fast32_t ทำให้ gcc ใช้ 64- บิต uint” โชคไม่ดีที่ฉันรู้สึกเสียใจไม่มีเวทย์มนตร์และไม่มีรหัสวิปัสสนาอยู่เบื้องหลังสิ่งเหล่านี้ ฉันยังไม่เห็นพวกเขาให้วิธีอื่นนอกเหนือจากที่เป็นตัวพิมพ์เดี่ยวสำหรับทุกที่ที่เป็นไปได้และทุกโปรแกรมบนแพลตฟอร์มทั้งหมด มีความเป็นไปได้ค่อนข้างที่จะใช้ความคิดบางอย่างที่อยู่เบื้องหลังตัวเลือกประเภทที่แน่นอน แต่คำจำกัดความเดียวสำหรับแต่ละประเภทอาจไม่เหมาะสมกับแอปพลิเคชันทุกประเภทที่เคยมี บางคนอ่านเพิ่มเติม: stackoverflow.com/q/4116297
Keno

2
@Keno นั่นเป็นเพราะsizeof(uint_fast32_t)จะต้องมีการกำหนดไว้ หากคุณไม่อนุญาตให้ทำคุณสามารถทำกลอุบายนั้นได้ แต่สามารถทำได้โดยใช้ส่วนขยายคอมไพเลอร์เท่านั้น
wizzwizz4

25

นี่ไม่ใช่คำตอบ แต่มันยากที่จะอ่านถ้าฉันแสดงความคิดเห็น

ฉันได้รับผลลัพธ์เหล่านี้ด้วยMac Pro ( Westmere 6-Cores Xeon 3.33 GHz) ฉันรวบรวมมันด้วยclang -O3 -msse4 -lstdc++ a.cpp -o a (-O2 รับผลลัพธ์เดียวกัน)

เสียงดังกราวด้วย uint64_t size=atol(argv[1])<<20;

unsigned    41950110000 0.811198 sec    12.9263 GB/s
uint64_t    41950110000 0.622884 sec    16.8342 GB/s

เสียงดังกราวด้วย uint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/s
uint64_t    41950110000 0.623685 sec    16.8126 GB/s

ฉันพยายาม:

  1. ย้อนกลับคำสั่งทดสอบผลที่ได้คือเหมือนกันดังนั้นจึงออกกฎปัจจัยแคช
  2. มีคำสั่งในการย้อนกลับ:for for (uint64_t i=size/8;i>0;i-=4)สิ่งนี้ให้ผลลัพธ์ที่เหมือนกันและพิสูจน์ว่าคอมไพล์นั้นฉลาดพอที่จะไม่หารขนาดด้วย 8 ทุกการวนซ้ำ (ตามที่คาดไว้)

นี่คือเดาป่าของฉัน:

ปัจจัยความเร็วมาในสามส่วน:

  • code cache: uint64_tเวอร์ชั่นมีขนาดรหัสที่ใหญ่กว่านี้ แต่ไม่มีผลกับซีออน Xeon ของฉัน ทำให้รุ่น 64 บิตช้าลง

  • คำแนะนำที่ใช้ หมายเหตุไม่เพียง แต่การนับลูป แต่จะเข้าถึงบัฟเฟอร์ด้วยดัชนีแบบ 32 บิตและ 64 บิตในสองเวอร์ชัน การเข้าถึงตัวชี้ด้วยออฟเซ็ต 64- บิตร้องขอการลงทะเบียนและการระบุแอดเดรส 64- บิตโดยเฉพาะในขณะที่คุณสามารถใช้งานได้ทันทีสำหรับออฟเซ็ต 32 บิต นี่อาจทำให้รุ่น 32 บิตเร็วขึ้น

  • คำแนะนำนั้นจะถูกปล่อยออกมาในคอมไพล์ 64- บิตเท่านั้น (นั่นคือ prefetch) ทำให้เร็วขึ้น 64 บิต

ปัจจัยสามประการพร้อมกันนั้นตรงกับผลลัพธ์ที่ขัดแย้งกัน


4
น่าสนใจคุณสามารถเพิ่มเวอร์ชันคอมไพเลอร์และแฟล็กคอมไพเลอร์ได้หรือไม่ สิ่งที่ดีที่สุดก็คือว่าในเครื่องของคุณผลลัพธ์จะหันไปรอบ ๆ คือใช้ u64 ได้เร็วขึ้น จนถึงตอนนี้ฉันไม่เคยคิดเลยว่าตัวแปร loop ของฉันจะเป็นอย่างไร แต่ดูเหมือนว่าฉันต้องคิดสองครั้งในครั้งต่อไป :)
gexicide

2
@ gexicide: ฉันจะไม่เรียกกระโดดจาก 16.8201 ถึง 16.8126 ทำให้ "เร็วขึ้น"
user541686

2
@ Mehrdad: กระโดดที่ฉันหมายถึงคือระหว่าง12.9และ16.8ดังนั้นจึงunsignedเร็วกว่าที่นี่ ในเกณฑ์มาตรฐานของฉันตรงกันข้ามคือกรณีที่ 26 สำหรับunsigned, 15 สำหรับuint64_t
gexicide

@ gexicide คุณสังเกตเห็นความแตกต่างในการกำหนดบัฟเฟอร์ [i] หรือไม่?
ขัดจังหวะ Non-maskable

@ Calvin: ไม่คุณหมายถึงอะไร?
gexicide

10

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

ดังนั้นระหว่างไปป์ไลน์สูงสุดและประสิทธิภาพการกระจายหลายครั้งและความล้มเหลวของกลไกเหล่านี้เรามีปัจจัยหกประการในการปฏิบัติงาน เป็นที่ทราบกันดีว่าความซับซ้อนของชุดคำสั่ง x86 ทำให้ง่ายต่อการแตกหัก เอกสารด้านบนมีตัวอย่างที่ดี:

ประสิทธิภาพของ Pentium 4 สำหรับการเลื่อนที่ถูกต้องแบบ 64 บิตนั้นแย่มาก การเลื่อนไปทางซ้าย 64 บิตและการเลื่อนแบบ 32 บิตทั้งหมดมีประสิทธิภาพที่ยอมรับได้ ปรากฏว่าเส้นทางข้อมูลจาก 32 บิตบนถึง 32 บิตต่ำกว่าของ ALU นั้นไม่ได้รับการออกแบบมาอย่างดี

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

ที่นี่ฉันเดาว่าเป็นความขัดแย้งสำหรับหน่วยจำนวนเต็ม: ที่ popcntคำนวณวนรอบและการคำนวณที่อยู่สามารถทำงานได้อย่างเต็มประสิทธิภาพด้วยตัวนับกว้าง 32 บิต แต่ตัวนับ 64 บิตทำให้เกิดการโต้เถียงและแผงลอยไปป์ไลน์ เนื่องจากมีทั้งหมดประมาณ 12 รอบอาจมี 4 รอบที่มีการแจกจ่ายหลายครั้งต่อการประมวลผลของลูปหนึ่งแผงลอยเดี่ยวอาจมีผลต่อเวลาในการรันโดยปัจจัย 2

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

ฉันรู้ว่านี่ไม่ใช่การวิเคราะห์ที่เข้มงวด แต่เป็นคำอธิบายที่น่าเชื่อถือ


2
น่าเสียดายที่นับตั้งแต่ (Core 2?) แทบไม่มีความแตกต่างด้านประสิทธิภาพระหว่างการดำเนินการจำนวนเต็ม 32- บิตและ 64- บิตยกเว้นการคูณ / หาร - ซึ่งไม่มีอยู่ในรหัสนี้
Mysticial

@Gene: โปรดทราบว่าทุกรุ่นเก็บขนาดไว้ใน register และไม่เคยอ่านมันจาก stack ใน loop ดังนั้นการคำนวณที่อยู่ไม่สามารถอยู่ในการผสมอย่างน้อยไม่ได้อยู่ในวง
gexicide

@ ยีน: คำอธิบายที่น่าสนใจแน่นอน! แต่มันไม่ได้อธิบายถึงจุด WTF หลัก: 64 บิตนั้นช้ากว่า 32 บิตเนื่องจากแผงลอยของท่อเป็นสิ่งหนึ่ง แต่ถ้าเป็นกรณีนี้รุ่น 64 บิตไม่ควรช้ากว่ารุ่น 32 บิตอย่างน่าเชื่อถือใช่ไหม คอมไพเลอร์ที่แตกต่างกันสามตัวปล่อยรหัสช้าแม้สำหรับรุ่น 32 บิตเมื่อใช้ขนาดบัฟเฟอร์ที่คอมไพล์เวลาคงที่ การเปลี่ยนขนาดบัฟเฟอร์ให้เป็นแบบสแตติกเปลี่ยนแปลงได้อย่างสมบูรณ์ มีแม้กระทั่งกรณีในเครื่องเพื่อนร่วมงานของฉัน (และในคำตอบของ Calvin) ซึ่งรุ่น 64 บิตเร็วขึ้นมาก! ดูเหมือนจะคาดเดาไม่ได้อย่างแน่นอน ..
gexicide

@ Mysticial นั่นคือจุดของฉัน ไม่มีความแตกต่างของประสิทธิภาพสูงสุดเมื่อไม่มีการช่วงชิงสำหรับ IU เวลารถบัส ฯลฯ การอ้างอิงแสดงให้เห็นอย่างชัดเจน ความขัดแย้งทำให้ทุกอย่างแตกต่าง นี่คือตัวอย่างจากวรรณกรรม Intel Core: "เทคโนโลยีใหม่ที่รวมอยู่ในการออกแบบคือ Macro-Ops Fusion ซึ่งรวมคำสั่ง x86 สองคำสั่งไว้ในการทำงานเดี่ยวขนาดเล็กตัวอย่างเช่นลำดับรหัสทั่วไปเช่นการเปรียบเทียบตามด้วยการกระโดดตามเงื่อนไข จะกลายเป็น micro-op เดียว แต่น่าเสียดายที่เทคโนโลยีนี้ใช้งานไม่ได้ในโหมด 64 บิต " ดังนั้นเราจึงมีอัตราส่วน 2: 1 ในความเร็วการประมวลผล
ยีน

@ ฆ่าตัวตายฉันเห็นสิ่งที่คุณพูด แต่คุณอนุมานมากกว่าที่ฉันหมายถึง ฉันกำลังบอกว่ารหัสที่ทำงานเร็วที่สุดคือการรักษาไปป์ไลน์และส่งคิวเต็ม สภาพนี้เปราะบาง การเปลี่ยนแปลงเล็กน้อยเช่นการเพิ่ม 32 บิตให้กับการไหลของข้อมูลทั้งหมดและการเรียงลำดับคำสั่งใหม่ก็เพียงพอที่จะทำให้เกิดการแตกหัก ในระยะสั้นการยืนยัน OP ที่เล่นซอและการทดสอบเป็นวิธีเดียวที่ถูกต้องไปข้างหน้า
ยีน

10

ฉันลองสิ่งนี้กับVisual Studio 2013 Expressโดยใช้ตัวชี้แทนดัชนีซึ่งเร่งกระบวนการขึ้นเล็กน้อย ฉันสงสัยว่านี่เป็นเพราะการกำหนดที่อยู่เป็น offset + register แทนที่จะเป็น offset + register + (register << 3) รหัส C ++

   uint64_t* bfrend = buffer+(size/8);
   uint64_t* bfrptr;

// ...

   {
      startP = chrono::system_clock::now();
      count = 0;
      for (unsigned k = 0; k < 10000; k++){
         // Tight unrolled loop with uint64_t
         for (bfrptr = buffer; bfrptr < bfrend;){
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
         }
      }
      endP = chrono::system_clock::now();
      duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
           << (10000.0*size)/(duration) << " GB/s" << endl;
   }

รหัสการประกอบ: r10 = bfrptr, r15 = bfrend, rsi = count, rdi = buffer, r13 = k:

$LL5@main:
        mov     r10, rdi
        cmp     rdi, r15
        jae     SHORT $LN4@main
        npad    4
$LL2@main:
        mov     rax, QWORD PTR [r10+24]
        mov     rcx, QWORD PTR [r10+16]
        mov     r8, QWORD PTR [r10+8]
        mov     r9, QWORD PTR [r10]
        popcnt  rdx, rax
        popcnt  rax, rcx
        add     rdx, rax
        popcnt  rax, r8
        add     r10, 32
        add     rdx, rax
        popcnt  rax, r9
        add     rsi, rax
        add     rsi, rdx
        cmp     r10, r15
        jb      SHORT $LL2@main
$LN4@main:
        dec     r13
        jne     SHORT $LL5@main

9

คุณลองส่งผ่าน-funroll-loops -fprefetch-loop-arraysไปยัง GCC หรือไม่

ฉันได้รับผลลัพธ์ต่อไปนี้ด้วยการเพิ่มประสิทธิภาพเพิ่มเติมเหล่านี้:

[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1
model name      : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz
[1829] /tmp/so_25078285 $ g++ --version|head -n1
g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3

[1829] /tmp/so_25078285 $ g++ -O3 -march=native -std=c++11 test.cpp -o test_o3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -funroll-loops -fprefetch-loop-arrays -std=c++11     test.cpp -o test_o3_unroll_loops__and__prefetch_loop_arrays

[1829] /tmp/so_25078285 $ ./test_o3 1
unsigned        41959360000     0.595 sec       17.6231 GB/s
uint64_t        41959360000     0.898626 sec    11.6687 GB/s

[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1
unsigned        41959360000     0.618222 sec    16.9612 GB/s
uint64_t        41959360000     0.407304 sec    25.7443 GB/s

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

7

คุณลองเคลื่อนย้ายขั้นตอนการลดลงนอกลูปหรือไม่? ตอนนี้คุณมีการพึ่งพาข้อมูลที่ไม่จำเป็นจริงๆ

ลอง:

  uint64_t subset_counts[4] = {};
  for( unsigned k = 0; k < 10000; k++){
     // Tight unrolled loop with unsigned
     unsigned i=0;
     while (i < size/8) {
        subset_counts[0] += _mm_popcnt_u64(buffer[i]);
        subset_counts[1] += _mm_popcnt_u64(buffer[i+1]);
        subset_counts[2] += _mm_popcnt_u64(buffer[i+2]);
        subset_counts[3] += _mm_popcnt_u64(buffer[i+3]);
        i += 4;
     }
  }
  count = subset_counts[0] + subset_counts[1] + subset_counts[2] + subset_counts[3];

คุณมีนามแฝงแปลก ๆ บางอย่างเกิดขึ้นซึ่งฉันไม่แน่ใจว่าเป็นไปตามกฎนามแฝงที่เข้มงวด


2
นั่นเป็นสิ่งแรกที่ฉันทำหลังจากอ่านคำถาม ทำลายห่วงโซ่การพึ่งพา ตามที่ปรากฏความแตกต่างของประสิทธิภาพจะไม่เปลี่ยนแปลง (บนคอมพิวเตอร์ของฉันอย่างน้อย - Intel Haswell กับ GCC 4.7.3)
Nils Pipenbrinck

1
@BenVoigt: มันสอดคล้องกับนามแฝงที่เข้มงวด void*และchar*เป็นสองประเภทที่อาจใช้นามแฝงเนื่องจากถือว่าเป็น "ตัวชี้ไปยังหน่วยความจำ" ความคิดของคุณเกี่ยวกับการลบการพึ่งพาข้อมูลนั้นดีสำหรับการเพิ่มประสิทธิภาพ แต่ไม่ตอบคำถาม และตามที่ @NilsPipenbrinck กล่าวว่าดูเหมือนจะไม่เปลี่ยนแปลงอะไรเลย
gexicide

@ gexicide: กฎนามแฝงที่เข้มงวดไม่สมมาตร คุณสามารถใช้เพื่อเข้าถึงchar* T[]คุณไม่สามารถใช้ a T*เพื่อเข้าถึง a ได้อย่างปลอดภัยchar[]และรหัสของคุณดูเหมือนจะทำในภายหลัง
Ben Voigt

@BenVoigt: แล้วคุณอาจจะไม่เคย savely mallocอาร์เรย์ของสิ่งที่เป็นผลตอบแทน malloc และคุณตีความว่ามันเป็นvoid* T[]และฉันก็ค่อนข้างแน่ใจvoid*และchar*มีความหมายเดียวกันกับนามแฝงที่เข้มงวด อย่างไรก็ตามฉันคิดว่านี่เป็นเรื่องที่ค่อนข้างไม่น่าสนใจที่นี่ :)
gexicide

1
โดยส่วนตัวฉันคิดว่าวิธีที่ถูกต้องคือuint64_t* buffer = new uint64_t[size/8]; /* type is clearly uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* aliasing a uint64_t[] with char* is safe */
Ben Voigt

6

TL; DR: ใช้ __builtinรินสิกแทน พวกเขาอาจช่วยได้

ฉันสามารถสร้างgcc4.8.4 (และ 4.7.3 บน gcc.godbolt.org) สร้างรหัสที่ดีที่สุดสำหรับสิ่งนี้โดยใช้__builtin_popcountllซึ่งใช้คำสั่งชุดประกอบเดิม แต่โชคดีและเกิดขึ้นเพื่อสร้างรหัสที่ไม่มีความคาดหมาย การขึ้นต่อเนื่องแบบวนซ้ำที่มีความยาวเนื่องจากข้อผิดพลาดในการพึ่งพาที่ผิด

ฉันไม่แน่ใจ 100% ของรหัสการเปรียบเทียบ แต่objdumpผลลัพธ์ดูเหมือนจะแชร์ความคิดเห็นของฉัน ฉันใช้ลูกเล่นอื่น ( ++ivs i++) เพื่อทำให้คอมไพเลอร์คลี่วงออกมาโดยที่ไม่มีmovlคำสั่ง (พฤติกรรมแปลก ๆ ฉันต้องพูด)

ผล:

Count: 20318230000  Elapsed: 0.411156 seconds   Speed: 25.503118 GB/s

รหัสเปรียบเทียบ:

#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t builtin_popcnt(const uint64_t* buf, size_t len){
  uint64_t cnt = 0;
  for(size_t i = 0; i < len; ++i){
    cnt += __builtin_popcountll(buf[i]);
  }
  return cnt;
}

int main(int argc, char** argv){
  if(argc != 2){
    printf("Usage: %s <buffer size in MB>\n", argv[0]);
    return -1;
  }
  uint64_t size = atol(argv[1]) << 20;
  uint64_t* buffer = (uint64_t*)malloc((size/8)*sizeof(*buffer));

  // Spoil copy-on-write memory allocation on *nix
  for (size_t i = 0; i < (size / 8); i++) {
    buffer[i] = random();
  }
  uint64_t count = 0;
  clock_t tic = clock();
  for(size_t i = 0; i < 10000; ++i){
    count += builtin_popcnt(buffer, size/8);
  }
  clock_t toc = clock();
  printf("Count: %lu\tElapsed: %f seconds\tSpeed: %f GB/s\n", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC)));
  return 0;
}

ตัวเลือกการรวบรวม:

gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench

รุ่น GCC:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

รุ่นเคอร์เนล Linux:

3.19.0-58-generic

ข้อมูล CPU:

processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 70
model name  : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHz
stepping    : 1
microcode   : 0xf
cpu MHz     : 2494.226
cache size  : 6144 KB
physical id : 0
siblings    : 1
core id     : 0
cpu cores   : 1
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm arat pln pts dtherm fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid xsaveopt
bugs        :
bogomips    : 4988.45
clflush size    : 64
cache_alignment : 64
address sizes   : 36 bits physical, 48 bits virtual
power management:

3
เป็นความโชคดีที่-funroll-loopsเกิดขึ้นกับการสร้างโค้ดที่ไม่ได้เป็นปัญหาคอขวดในการพึ่งพาของลูปที่ดำเนินการโดยห่วงซึ่งสร้างขึ้นโดยpopcntdep ที่ผิดพลาดของ การใช้คอมไพเลอร์เวอร์ชันเก่าที่ไม่รู้เกี่ยวกับการอ้างอิงผิด ๆ นั้นเป็นความเสี่ยง หากไม่มี-funroll-loopsการวนซ้ำ gcc 4.8.5 จะทำให้เกิดปัญหาคอขวดในเวลาแฝง popcnt แทนปริมาณงานเนื่องจากมันนับrdxรวม รหัสเดียวกันรวบรวมโดย gcc 4.9.3เพิ่มxor edx,edxเพื่อทำลายห่วงโซ่การพึ่งพา
Peter Cordes

3
ด้วยคอมไพเลอร์เก่ารหัสของคุณจะยังคงเสี่ยงต่อการเปลี่ยนแปลงของประสิทธิภาพเดียวกันกับที่ OP พบ: การเปลี่ยนแปลงเล็กน้อยที่ดูเหมือนจะทำให้ gcc ช้าลงเพราะไม่รู้ว่ามันจะทำให้เกิดปัญหา การค้นหาสิ่งที่เกิดขึ้นกับการทำงานในกรณีหนึ่งในคอมไพเลอร์เก่าไม่ใช่คำถาม
Peter Cordes

2
สำหรับบันทึกx86intrin.hของ_mm_popcnt_*ฟังก์ชั่นใน GCC จะห่อรอบ inlined กวาดต้อน__builtin_popcount* ; การทำอินไลน์ควรทำให้สิ่งหนึ่งเทียบเท่ากับอีกสิ่งหนึ่งอย่างแน่นอน ฉันสงสัยอย่างมากว่าคุณจะเห็นความแตกต่างที่อาจเกิดจากการสลับระหว่างพวกเขา
ShadowRanger

-2

ก่อนอื่นลองประเมินประสิทธิภาพสูงสุด - ตรวจสอบ https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdfโดยเฉพาะอย่างยิ่งภาคผนวก C.

ในกรณีของคุณเป็นตาราง C-10 ที่แสดงคำสั่ง POPCNT มีเวลาในการตอบสนอง = 3 นาฬิกาและทรูพุท = 1 นาฬิกา ปริมาณงานแสดงอัตราสูงสุดของคุณในนาฬิกา (คูณด้วยความถี่หลักและ 8 ไบต์ในกรณีของ popcnt64 เพื่อรับหมายเลขแบนด์วิดท์ที่ดีที่สุดของคุณ)

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

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

อย่างไรก็ตามในกรณีของคุณเพียงแค่เขียนโค้ดวิธีที่เหมาะสมจะกำจัดความซับซ้อนเหล่านี้ทั้งหมด แทนที่จะสะสมให้กับตัวแปรนับเดียวกันเพียงสะสมให้กับตัวแปรอื่น (เช่น count0, count1, ... count8) และสรุปผลรวมในตอนท้าย หรือแม้กระทั่งสร้างอาร์เรย์ของจำนวน [8] และสะสมองค์ประกอบของมัน - บางทีมันอาจจะถูกทำให้เป็นเวกเตอร์และคุณจะได้รับปริมาณงานที่มากขึ้น

PS และไม่เคยวิ่งเกณฑ์มาตรฐานเป็นครั้งที่สองก่อนอุ่นเครื่องแกนแล้ววิ่งวนเป็นเวลาอย่างน้อย 10 วินาทีหรือดีกว่า 100 วินาที ไม่เช่นนั้นคุณจะทดสอบเฟิร์มแวร์การจัดการพลังงานและการปรับใช้ DVFS ในฮาร์ดแวร์ :)

PPS ฉันได้ยินการถกเถียงกันว่าจะใช้เวลานานเท่าไหร่ในการวัดมาตรฐาน คนที่ฉลาดที่สุดส่วนใหญ่ยังถามว่าทำไม 10 วินาทีไม่ใช่ 11 หรือ 12 ฉันควรยอมรับว่านี่เป็นเรื่องตลกในทางทฤษฎี ในทางปฏิบัติคุณเพียงแค่ไปและทำงานมาตรฐานร้อยครั้งในแถวและบันทึกการเบี่ยงเบน นั่นคือเรื่องตลก คนส่วนใหญ่เปลี่ยนแหล่งที่มาและเรียกใช้ผู้พิพากษาหลังจากนั้นเพียงครั้งเดียวเพื่อจับภาพบันทึกการทำงานใหม่ ทำสิ่งที่ถูกต้อง

ยังไม่มั่นใจ เพียงใช้เกณฑ์มาตรฐาน C-version ข้างต้นโดย assp1r1n3 ( https://stackoverflow.com/a/37026212/9706746 ) และลอง 100 แทน 10000 ในการลองส่งซ้ำ

7960X ของฉันแสดงด้วย RETRY = 100:

นับ: 203182300 เวลาผ่านไป: 0.008385 วินาทีความเร็ว: 12.505379 GB / s

นับ: 203182300 ที่ผ่านไป: 0.011063 วินาทีความเร็ว: 9.478225 GB / s

นับ: 203182300 เวลาผ่านไป: 0.011188 วินาทีความเร็ว: 9.372327 GB / s

นับ: 203182300 เวลาผ่านไป: 0.010393 วินาทีความเร็ว: 10.089252 GB / s

นับ: 203182300 เวลาผ่านไป: 0.009076 วินาทีความเร็ว: 11.553283 GB / s

ด้วย RETRY = 10000:

นับ: 20318230000 ผ่านไป: 0.661791 วินาทีความเร็ว: 15.844519 GB / s

นับ: 20318230000 ผ่านไป: 0.665422 วินาทีความเร็ว: 15.758060 GB / s

นับ: 20318230000 ที่ผ่านไป: 0.660983 วินาทีความเร็ว: 15.863888 GB / s

นับ: 20318230000 ผ่านไป: 0.665337 วินาทีความเร็ว: 15.760073 GB / s

นับ: 20318230000 ที่ผ่านไป: 0.662138 วินาทีความเร็ว: 15.836215 GB / s

PPPS ในที่สุดกับ "คำตอบที่ยอมรับ" และ mistery อื่น ๆ ;-)

ลองใช้คำตอบของ assp1r1n3 - เขามีแกน 2.5Ghz POPCNT มีเวลา 1 นาฬิกาผ่านรหัสของเขาใช้ popcnt แบบ 64 บิต ดังนั้นคณิตศาสตร์คือ 2.5Ghz * 1 นาฬิกา * 8 ไบต์ = 20 GB / s สำหรับการตั้งค่าของเขา เขาเห็น 25Gb / s อาจเป็นเพราะ turbo boost ประมาณ 3Ghz

ดังนั้นไปที่ ark.intel.com และค้นหา i7-4870HQ: https://ark.intel.com/products/83504/Intel-Core-i7-4870HQ-Processor-6M-Cache-up-to-3-70 -GHz-? q = i7-4870HQ

แกนนั้นสามารถทำงานได้สูงถึง 3.7 กิกะเฮิร์ตซ์และอัตราสูงสุดจริงคือ 29.6 GB / s สำหรับฮาร์ดแวร์ของเขา ดังนั้นอีก 4GB / s อยู่ที่ไหน บางทีมันใช้กับวนรอบลอจิกและโค้ดที่ล้อมรอบอื่น ๆ ภายในการวนซ้ำแต่ละครั้ง

ตอนนี้อยู่ที่ไหนนี้พึ่งพาเท็จ? ฮาร์ดแวร์ทำงานในอัตราสูงสุดเกือบ บางทีคณิตศาสตร์ของฉันไม่ดีมันเกิดขึ้นบางครั้ง :)

PPPPPS ผู้คนที่แนะนำ HW errata เป็นผู้ร้ายดังนั้นฉันทำตามคำแนะนำและสร้างตัวอย่างแบบอินไลน์โปรดดูด้านล่าง

ใน 7960X ของฉันเวอร์ชันแรก (ที่มีเอาต์พุตเดี่ยวไปยัง cnt0) ทำงานที่ 11MB / s รุ่นที่สอง (ที่มีเอาต์พุตเป็น cnt0, cnt1, cnt2 และ cnt3) ทำงานที่ 33MB / s และหนึ่งสามารถพูดได้ - voila! มันขึ้นอยู่กับการส่งออก

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

uint64_t builtin_popcnt1a(const uint64_t* buf, size_t len) 
{
    uint64_t cnt0, cnt1, cnt2, cnt3;
    cnt0 = cnt1 = cnt2 = cnt3 = 0;
    uint64_t val = buf[0];
    #if 0
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0)
        : "q" (val)
        :
        );
    #else
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %5, %1\n\t"
            "popcnt %5, %2\n\t"
            "popcnt %5, %3\n\t"
            "popcnt %5, %4\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0), "=q" (cnt1), "=q" (cnt2), "=q" (cnt3)
        : "q" (val)
        :
        );
    #endif
    return cnt0;
}

หากคุณกำหนดเวลาในรอบสัญญาณนาฬิกาหลัก (แทนที่จะเป็นวินาที) 1 วินาทีจะมีเวลาเหลือเฟือสำหรับการวนรอบ CPU ที่ถูกผูกไว้ แม้แต่ 100 มิลลิวินาทีก็ดีสำหรับการค้นหาความแตกต่างที่สำคัญหรือการตรวจสอบเคาน์เตอร์ที่สมบูรณ์แบบสำหรับการนับจำนวน uop โดยเฉพาะอย่างยิ่งใน Skylake ที่การจัดการสถานะ P ของฮาร์ดแวร์ทำให้สามารถเพิ่มความเร็วสัญญาณนาฬิกาสูงสุดเป็นไมโครวินาทีหลังจากเริ่มโหลด
Peter Cordes

เสียงดังกราวสามารถปรับเวกเตอร์อัตโนมัติ__builtin_popcountlด้วย AVX2 vpshufbและไม่จำเป็นต้องมีตัวสะสมหลายตัวในแหล่ง C เพื่อทำเช่นนั้น ผมไม่แน่ใจว่าเกี่ยวกับ_mm_popcnt_u64; นั่นอาจทำให้เป็นอัตโนมัติแบบเวกเตอร์ด้วย AVX512-VPOPCNT เท่านั้น (ดูการนับ 1 บิต (จำนวนประชากร) ของข้อมูลขนาดใหญ่โดยใช้ AVX-512 หรือ AVX-2 /)
Peter Cordes

แต่อย่างไรก็ตามมองของ Intel คู่มือการเพิ่มประสิทธิภาพจะไม่ช่วย: popcntเป็นแสดงให้เห็นว่าคำตอบที่ได้รับการยอมรับปัญหาคือมีการพึ่งพาการส่งออกที่ไม่คาดคิดสำหรับ นี่เป็นเอกสารใน errata ของ Intel สำหรับสถาปัตยกรรมขนาดเล็กล่าสุดของพวกเขา แต่ฉันคิดว่าไม่ใช่ในเวลานั้น การวิเคราะห์ dep-chain ของคุณจะล้มเหลวหากมีการอ้างอิงผิดพลาดที่ไม่คาดคิดดังนั้นคำตอบนี้เป็นคำแนะนำทั่วไปที่ดี แต่ไม่สามารถใช้ได้ที่นี่
Peter Cordes

1
คุณล้อเล่นกับฉันไหม ฉันไม่จำเป็นต้อง "เชื่อ" ในสิ่งที่ฉันสามารถวัดได้ด้วยตัวนับประสิทธิภาพในวง asm ที่เขียนด้วยมือ มันเป็นแค่ข้อเท็จจริง ฉันมีการทดสอบและ Skylake คงพึ่งพาเท็จlzcnt/ แต่ไม่ได้สำหรับtzcnt popcntดู SKL029 erratum ของอินเทลในintel.com/content/dam/www/public/us/en/documents/... นอกจากนี้gcc.gnu.org/bugzilla/show_bug.cgi?id=62011ก็คือ "ได้รับการแก้ไขแล้ว" ไม่ใช่ "ไม่ถูกต้อง" ไม่มีพื้นฐานสำหรับการอ้างสิทธิ์ของคุณว่าไม่มีการพึ่งพาผลลัพธ์ใน HW
Peter Cordes

1
หากคุณทำลูปง่าย ๆ เช่นpopcnt eax, edx/ dec ecx / jnzคุณคาดว่ามันจะทำงานที่ 1 ต่อนาฬิกาโดยมีปัญหาคอขวดในปริมาณงานของ popcnt และปริมาณงานที่รับ แต่จริงๆแล้วมันรันที่ 1 ต่อ 3 นาฬิกาที่ติดขัดในpopcntเวลาแฝงสำหรับการเขียนทับ EAX ซ้ำ ๆ แม้ว่าคุณจะคาดหวังว่ามันจะเป็นแบบเขียนอย่างเดียว คุณมี Skylake ดังนั้นคุณสามารถลองด้วยตัวเองได้
Peter Cordes

-3

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

ทำไมการstaticเปลี่ยนแปลงนั้นถึงมีประสิทธิภาพ?

บรรทัดในคำถาม: uint64_t size = atol(argv[1])<<20;

คำตอบสั้น ๆ

ฉันจะดูแอสเซมบลีที่สร้างขึ้นสำหรับการเข้าถึงsizeและดูว่ามีขั้นตอนพิเศษของตัวชี้ทางอ้อมที่เกี่ยวข้องกับรุ่นที่ไม่คงที่

คำตอบยาว ๆ

เนื่องจากมีตัวแปรเพียงชุดเดียวไม่ว่าจะมีการประกาศstaticหรือไม่และขนาดก็ไม่เปลี่ยนแปลงดังนั้นผมจึงตั้งทฤษฎีว่าความแตกต่างคือตำแหน่งของหน่วยความจำที่ใช้ในการสำรองตัวแปรพร้อมกับตำแหน่งที่ใช้ในรหัสต่อไป ลง.

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

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


2
ดูเหมือนว่าฉันจะใช้staticการเปลี่ยนแปลงการจัดสรรการลงทะเบียนสำหรับฟังก์ชั่นในทางที่มีผลต่อการพึ่งพาเอาต์พุตที่ผิดพลาดของpopcntIntel CPUs ที่ OP กำลังทดสอบอยู่โดยคอมไพเลอร์ที่ไม่รู้ว่าจะหลีกเลี่ยง (เนื่องจากหลุมบ่อประสิทธิภาพในซีพียู Intel ยังไม่ได้ค้นพบ) คอมไพเลอร์สามารถเก็บstaticตัวแปรโลคัลในรีจิสเตอร์เช่นเดียวกับตัวแปรหน่วยเก็บข้อมูลอัตโนมัติ แต่ถ้าพวกเขาไม่ปรับการสมมติว่าmainจะทำงานเพียงครั้งเดียวก็จะมีผล code-gen (เนื่องจากค่าถูกตั้งค่าโดยการโทรครั้งแรกเท่านั้น)
Peter Cordes

1
อย่างไรก็ตามความแตกต่างด้านประสิทธิภาพระหว่าง[RIP + rel32]และ[rsp + 42]โหมดการกำหนดแอดเดรสนั้นค่อนข้างเล็กน้อยสำหรับกรณีส่วนใหญ่ cmp dword [RIP+rel32], immediateไม่สามารถไมโครฟิวส์เป็นโหลดเดียว + cmp uop แต่ฉันไม่คิดว่ามันจะเป็นปัจจัย อย่างที่ฉันบอกไปข้างในลูปมันอาจจะยังอยู่ในทะเบียน แต่การปรับแต่ง C ++ อาจหมายถึงตัวเลือกคอมไพเลอร์ที่แตกต่างกัน
ปีเตอร์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.