รหัส C ++ สำหรับการทดสอบการคาดคะเนของ Collatz เร็วกว่าชุดประกอบที่เขียนด้วยมือ - ทำไม?


833

ฉันเขียนโซลูชันทั้งสองนี้สำหรับProject Euler Q14ในแอสเซมบลีและใน C ++ พวกมันเหมือนกันกับวิธีเดรัจฉานแรงแบบเดียวกันสำหรับการทดสอบการคาดคะเนของโคลลาตซ์ โซลูชันการประกอบถูกประกอบด้วย

nasm -felf64 p14.asm && gcc p14.o -o p14

คอมไพล์ด้วย C ++

g++ p14.cpp -o p14

การชุมนุม p14.asm

section .data
    fmt db "%d", 10, 0

global main
extern printf

section .text

main:
    mov rcx, 1000000
    xor rdi, rdi        ; max i
    xor rsi, rsi        ; i

l1:
    dec rcx
    xor r10, r10        ; count
    mov rax, rcx

l2:
    test rax, 1
    jpe even

    mov rbx, 3
    mul rbx
    inc rax
    jmp c1

even:
    mov rbx, 2
    xor rdx, rdx
    div rbx

c1:
    inc r10
    cmp rax, 1
    jne l2

    cmp rdi, r10
    cmovl rdi, r10
    cmovl rsi, rcx

    cmp rcx, 2
    jne l1

    mov rdi, fmt
    xor rax, rax
    call printf
    ret

C ++, p14.cpp

#include <iostream>

using namespace std;

int sequence(long n) {
    int count = 1;
    while (n != 1) {
        if (n % 2 == 0)
            n /= 2;
        else
            n = n*3 + 1;

        ++count;
    }

    return count;
}

int main() {
    int max = 0, maxi;
    for (int i = 999999; i > 0; --i) {
        int s = sequence(i);
        if (s > max) {
            max = s;
            maxi = i;
        }
    }

    cout << maxi << endl;
}

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

รหัส C ++ มีโมดูลัสทุกเทอมและหารทุกเทอมโดยที่แอสเซมบลีเป็นเพียงส่วนเดียวต่อเทอม

แต่แอสเซมบลีใช้เวลาโดยเฉลี่ย 1 วินาทีนานกว่าโซลูชัน C ++ ทำไมนี้ ฉันขอจากความอยากรู้ส่วนใหญ่

เวลาดำเนินการ

ระบบของฉัน: 64 บิต Linux บน 1.4 GHz Intel Celeron 2955U (Haswell microarchitecture)


232
คุณตรวจสอบรหัสประกอบที่ GCC สร้างขึ้นสำหรับโปรแกรม C ++ ของคุณหรือไม่
ruakh

69
คอมไพล์ด้วย-Sเพื่อรับแอสเซมบลีที่คอมไพเลอร์สร้างขึ้น คอมไพเลอร์นั้นฉลาดพอที่จะรู้ว่าโมดูลัสทำหน้าที่หารในเวลาเดียวกัน
user3386109

267
ฉันคิดว่าทางเลือกของคุณคือ1เทคนิคการวัดของคุณมีข้อบกพร่อง2คอมไพเลอร์เขียนชุดประกอบที่ดีกว่าที่คุณหรือ3คอมไพเลอร์ใช้เวทย์มนตร์
Galik


18
@jefferson คอมไพเลอร์สามารถใช้กำลังดุร้ายได้เร็วขึ้น ตัวอย่างเช่นอาจมีคำแนะนำ SSE
user253751

คำตอบ:


1896

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

ดูคู่มือประกอบการเพิ่มประสิทธิภาพของ Agner Fogเพื่อเรียนรู้วิธีเขียน asm อย่างมีประสิทธิภาพ นอกจากนี้เขายังมีตารางคำแนะนำและคู่มือ microarch สำหรับรายละเอียดเฉพาะสำหรับ CPU ที่เฉพาะเจาะจง ดูยัง แท็ก wiki สำหรับลิงค์ perf เพิ่มเติม

ดูคำถามทั่วไปเกี่ยวกับการแปลคอมไพเลอร์ด้วย asm ที่เขียนด้วยมือ: ภาษาแอสเซมบลีแบบอินไลน์ช้ากว่าโค้ด C ++ ดั้งเดิมหรือไม่ . TL: DR: ใช่ถ้าคุณทำผิด (เช่นคำถามนี้)

โดยปกติแล้วคุณปรับกำลังปล่อยให้คอมไพเลอร์ทำสิ่งที่ตนโดยเฉพาะอย่างยิ่งถ้าคุณพยายามที่จะเขียน c ++ ที่สามารถรวบรวมได้อย่างมีประสิทธิภาพ ดูด้วยว่าการประกอบเร็วกว่าภาษาที่คอมไพล์หรือไม่ . หนึ่งในคำตอบเชื่อมโยงไปยังสไลด์ที่เรียบร้อยเหล่านี้แสดงให้เห็นว่าคอมไพเลอร์ C ต่าง ๆ ปรับแต่งฟังก์ชั่นที่เรียบง่ายด้วยเทคนิคสุดเจ๋ง CppCon2017 ของ Matt Godbolt พูดคุยว่า " เมื่อเร็ว ๆ นี้คอมไพเลอร์ของฉันทำอะไรให้ฉันบ้าง? การคลายสลักเกลียวของ Compiler ” อยู่ในหลอดเลือดดำที่คล้ายกัน


even:
    mov rbx, 2
    xor rdx, rdx
    div rbx

บน Intel Haswell div r64มี 36 uops โดยมีเวลาแฝงอยู่ที่ 32-96 รอบและปริมาณงานหนึ่งรอบต่อ 21-74 รอบ (บวก 2 uops เพื่อตั้งค่า RBX และศูนย์ RDX แต่การดำเนินการที่ไม่เป็นไปตามคำสั่งสามารถเรียกใช้งานก่อนหน้าได้) คำแนะนำแบบนับจำนวนสูงเช่น DIV นั้นเป็นไมโครโค้ดซึ่งอาจทำให้เกิดปัญหาคอขวดส่วนหน้า ในกรณีนี้เวลาในการตอบสนองเป็นปัจจัยที่มีความเกี่ยวข้องมากที่สุดเนื่องจากเป็นส่วนหนึ่งของห่วงโซ่การพึ่งพาแบบวนรอบ

shr rax, 1ทำส่วนที่ไม่ได้ลงนามเดียวกัน: มันคือ 1 uop, กับ 1c latency , และสามารถรัน 2 ต่อรอบสัญญาณนาฬิกา

สำหรับการเปรียบเทียบการแบ่งแบบ 32 บิตนั้นเร็วกว่า แต่ก็ยังน่ากลัวอยู่ idiv r32คือ 9 uops, 22-29c latency, และหนึ่งต่อ 8-11c throughput บน Haswell


อย่างที่คุณเห็นจากการดู-O0เอาต์พุต asm ของ gcc ( Godbolt compiler explorer ) จะใช้คำสั่งกะเท่านั้น clang -O0รวบรวมอย่างไร้เดียงสาอย่างที่คุณคิดแม้กระทั่งใช้ 64- บิต IDIV สองครั้ง (เมื่อทำการออปติไมซ์คอมไพเลอร์จะใช้ทั้งสองเอาต์พุตของ IDIV เมื่อแหล่งที่มาทำการหารและโมดูลัสด้วยตัวถูกดำเนินการเดียวกันถ้าพวกเขาใช้ IDIV เลย)

GCC ไม่มีโหมดที่ไร้เดียงสาทั้งหมด; มันจะแปลงผ่าน GIMPLE เสมอซึ่งหมายความว่า "การเพิ่มประสิทธิภาพ" บางอย่างไม่สามารถปิดใช้งานได้ ซึ่งรวมถึงการจดจำการหารโดยค่าคงที่และการใช้กะ (กำลัง 2) หรือการผกผันการคูณจุดคงที่ (ไม่ใช่กำลัง 2) เพื่อหลีกเลี่ยง IDIV (ดูdiv_by_13ในลิงค์ godbolt ด้านบน)

gcc -Os(การเพิ่มประสิทธิภาพขนาด) ไม่ใช้ IDIV สำหรับพลังงานที่ไม่ของ 2 ส่วนที่น่าเสียดายที่แม้ในกรณีที่รหัสผกผันเป็นเพียงขนาดใหญ่กว่าเล็กน้อย แต่ได้เร็วขึ้นมาก


ช่วยคอมไพเลอร์

(สรุปสำหรับกรณีนี้: ใช้uint64_t n)

ก่อนอื่นมันเป็นเรื่องที่น่าสนใจเพียงอย่างเดียวที่จะมองหาคอมไพเลอร์เอาท์พุทที่ดีที่สุด ( -O3) -O0ความเร็วนั้นไม่มีความหมายโดยทั่วไป

ดูที่เอาต์พุต asm ของคุณ (บน Godbolt หรือดูวิธีการ "ขจัดเสียงรบกวน" ออกจากเอาต์พุตชุดประกอบ GCC / เสียงดังกราวด์? ) เมื่อคอมไพเลอร์ไม่ได้ทำให้รหัสที่เหมาะสมในสถานที่แรก: การเขียนของคุณ C / C ++ แหล่งที่มาในทางที่คู่มือการคอมไพเลอร์ในการทำรหัสดีกว่ามักจะเป็นวิธีที่ดีที่สุด คุณต้องรู้จัก asm และรู้ว่าอะไรมีประสิทธิภาพ แต่คุณใช้ความรู้นี้ทางอ้อม คอมไพเลอร์ยังเป็นแหล่งความคิดที่ดี: บางครั้งเสียงดังกราวจะทำอะไรที่เจ๋ง ๆ และคุณสามารถถือ gcc ทำสิ่งเดียวกันได้: ดูคำตอบนี้และสิ่งที่ฉันทำกับลูปที่ไม่ได้ควบคุมในโค้ดของ @ Veedrac ด้านล่าง)

วิธีการนี้เป็นแบบพกพาและในอีก 20 ปีผู้รวบรวมในอนาคตสามารถรวบรวมสิ่งที่มีประสิทธิภาพในฮาร์ดแวร์ในอนาคต (x86 หรือไม่) อาจใช้ส่วนขยาย ISA ใหม่หรือปรับเวกเตอร์อัตโนมัติ x86-64 asm ที่เขียนด้วยมือเมื่อ 15 ปีที่แล้วมักจะไม่ได้รับการปรับให้เหมาะสมสำหรับ Skylake เช่นการเปรียบเทียบและการรวมตัวของแมโครฟิวชั่นสาขาไม่มีอยู่ในตอนนั้น สิ่งที่ดีที่สุดในตอนนี้สำหรับ asm ที่สร้างขึ้นด้วยมือสำหรับ microarchitecture หนึ่งอาจไม่เหมาะสำหรับ CPU ปัจจุบันและอนาคตอื่น ๆ ความเห็นเกี่ยวกับคำตอบของ @ johnfoundกล่าวถึงความแตกต่างที่สำคัญระหว่าง AMD Bulldozer และ Intel Haswell ซึ่งมีผลอย่างมากต่อรหัสนี้ แต่ในทางทฤษฎีg++ -O3 -march=bdver3และg++ -O3 -march=skylakeจะทำสิ่งที่ถูกต้อง (หรือ-march=native.) หรือ-mtune=...เพียงแค่ปรับแต่งโดยไม่ต้องใช้คำสั่งที่ CPU อื่นอาจไม่รองรับ

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

asm ที่เขียนด้วยมือเป็นกล่องดำสำหรับเครื่องมือเพิ่มประสิทธิภาพดังนั้นการแพร่กระจายคงที่ไม่ทำงานเมื่ออินไลน์ทำให้อินพุทเป็นค่าคงที่เวลาคอมไพล์ การเพิ่มประสิทธิภาพอื่น ๆ ยังได้รับผลกระทบ อ่านhttps://gcc.gnu.org/wiki/DontUseInlineAsmก่อนใช้ asm (และหลีกเลี่ยง MSM แบบอินไลน์ asm: อินพุต / เอาท์พุตจะต้องผ่านหน่วยความจำที่เพิ่มค่าใช้จ่าย )

ในกรณีนี้ : คุณnมีประเภทที่เซ็นชื่อแล้วและ gcc ใช้ลำดับ SAR / SHR / ADD ที่ให้การปัดเศษที่ถูกต้อง (IDIV และ arithmetic-shift "round" แตกต่างกันสำหรับอินพุตเชิงลบโปรดดูรายการป้อนด้วยตนเองของSAR insn set ) (IDK ถ้า gcc พยายามและล้มเหลวในการพิสูจน์ว่าnไม่สามารถลบได้หรืออะไรการลงชื่อล้นเกินนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นจึงควรจะสามารถทำได้)

คุณควรจะใช้uint64_t nมันก็แค่ SHR ดังนั้นจึงพกพาไปยังระบบที่longมีเพียง 32 บิต (เช่น x86-64 Windows)


BTW, เอาต์พุต asm ที่ปรับปรุงแล้วของ gcc ดูค่อนข้างดี (โดยใช้)unsigned long n : การวนรอบภายในที่อินไลน์เข้าmain()ทำเช่นนี้:

 # from gcc5.4 -O3  plus my comments

 # edx= count=1
 # rax= uint64_t n

.L9:                   # do{
    lea    rcx, [rax+1+rax*2]   # rcx = 3*n + 1
    mov    rdi, rax
    shr    rdi         # rdi = n>>1;
    test   al, 1       # set flags based on n%2 (aka n&1)
    mov    rax, rcx
    cmove  rax, rdi    # n= (n%2) ? 3*n+1 : n/2;
    add    edx, 1      # ++count;
    cmp    rax, 1
    jne   .L9          #}while(n!=1)

  cmp/branch to update max and maxi, and then do the next n

วงในเป็นแบบไร้สาขาและเส้นทางวิกฤตของห่วงโซ่การพึ่งพาแบบวนรอบที่ดำเนินการคือ:

  • LEA 3 ส่วนประกอบ (3 รอบ)
  • cmov (2 รอบใน Haswell, 1c บน Broadwell หรือใหม่กว่า)

ทั้งหมด: 5 รอบต่อย้ำแฝงคอขวด การดำเนินการที่ไม่เป็นไปตามสั่งจะดูแลทุกอย่างอื่นควบคู่ไปกับสิ่งนี้ (ในทางทฤษฎี: ฉันไม่ได้ทดสอบกับเคาน์เตอร์ที่สมบูรณ์แบบเพื่อดูว่ามันทำงานที่ 5c / iter จริงๆ

อินพุต FLAGS ของcmov(สร้างโดย TEST) นั้นเร็วกว่าการสร้างมากกว่าอินพุต RAX (จาก LEA-> MOV) ดังนั้นจึงไม่ได้อยู่ในเส้นทางวิกฤติ

ในทำนองเดียวกัน MOV-> SHR ที่สร้างอินพุต RDI ของ CMOV อยู่นอกเส้นทางที่สำคัญเพราะมันเร็วกว่า LEA เช่นกัน MOV บน IvyBridge และใหม่กว่ามีเวลาแฝงเป็นศูนย์ (จัดการเมื่อลงทะเบียนเปลี่ยนชื่อ) (มันยังคงใช้ uop และสล็อตในไปป์ไลน์ดังนั้นจึงไม่ฟรีเพียงไม่มีเวลาในการตอบสนอง) MOV พิเศษในเครือข่าย LEA dep เป็นส่วนหนึ่งของคอขวดในซีพียูอื่น ๆ

cmp / jne ยังไม่ได้เป็นส่วนหนึ่งของเส้นทางวิกฤติ: มันไม่ได้เป็นแบบวนรอบเนื่องจากการพึ่งพาการควบคุมได้รับการจัดการด้วยการคาดคะเนสาขา + การดำเนินการเก็งกำไรซึ่งแตกต่างจากการอ้างอิงข้อมูลบนเส้นทางวิกฤติ


ตีคอมไพเลอร์

GCC ทำได้ค่อนข้างดีที่นี่ มันสามารถบันทึกหนึ่งไบต์รหัสโดยใช้inc edxแทนadd edx, 1เนื่องจากไม่มีใครสนใจ P4 และการอ้างอิงเท็จสำหรับคำแนะนำการปรับเปลี่ยนบางส่วนการตั้งค่าสถานะ

มันยังสามารถบันทึกทุกคำแนะนำ MOV และการทดสอบ: SHR ชุด CF = บิตขยับตัวออกมาเพื่อให้เราสามารถใช้cmovcแทน/testcmovz

 ### Hand-optimized version of what gcc does
.L9:                       #do{
    lea     rcx, [rax+1+rax*2] # rcx = 3*n + 1
    shr     rax, 1         # n>>=1;    CF = n&1 = n%2
    cmovc   rax, rcx       # n= (n&1) ? 3*n+1 : n/2;
    inc     edx            # ++count;
    cmp     rax, 1
    jne     .L9            #}while(n!=1)

ดูคำตอบของ @ johnfound สำหรับเคล็ดลับอันชาญฉลาดอื่น: ลบ CMP โดยการแยกผลลัพธ์ธงของ SHR และใช้กับ CMOV: ศูนย์เฉพาะถ้า n เป็น 1 (หรือ 0) เพื่อเริ่มต้นด้วย (ความจริงแล้วสนุก: SHR พร้อม count! = 1 ใน Nehalem หรือก่อนหน้านี้ทำให้แผงลอยถ้าคุณอ่านผลลัพธ์ธงนั่นเป็นวิธีที่พวกเขาทำให้มันเป็นแบบ single-uop การเข้ารหัสพิเศษแบบ shift-by-1 ทำได้ดี)

การหลีกเลี่ยง MOV ไม่ได้ช่วยในเรื่องเวลาแฝงเลยใน Haswell ( MOV ของ x86 สามารถ "ฟรี" ได้หรือไม่ทำไมฉันถึงทำซ้ำไม่ได้เลย? ) มันช่วยอย่างมีนัยสำคัญเกี่ยวกับซีพียูเช่น Intel pre-IvB และ AMD Bulldozer-family ซึ่ง MOV ไม่ได้มีความหน่วงแฝง คำสั่ง MOV ที่เสียไปของคอมไพเลอร์จะส่งผลกระทบต่อเส้นทางที่สำคัญ คอมเพล็กซ์ของ LEA และ CMOV ของ BD มีทั้งเวลาแฝงที่ต่ำกว่า (2c และ 1c ตามลำดับ) ดังนั้นจึงเป็นความล่าช้าที่ใหญ่กว่า นอกจากนี้ปัญหาคอขวดสำหรับปริมาณงานกลายเป็นปัญหาเนื่องจากมีเพียง ALU จำนวนเต็มสองตัว ดูคำตอบของ @ johnfoundซึ่งเขามีผลการจับเวลาจากซีพียู AMD

แม้แต่ใน Haswell เวอร์ชันนี้อาจช่วยได้เล็กน้อยโดยหลีกเลี่ยงความล่าช้าบางครั้งซึ่ง uop ที่ไม่สำคัญขโมยพอร์ตการเรียกใช้จากพอร์ตที่สำคัญบนเส้นทางที่สำคัญ (สิ่งนี้เรียกว่าข้อขัดแย้งของทรัพยากร) นอกจากนี้ยังบันทึกการลงทะเบียนซึ่งอาจช่วยเมื่อทำnค่าหลายค่าในแบบคู่ขนานในการวนซ้ำ interleaved (ดูด้านล่าง)

เวลาแฝงของ LEA ขึ้นอยู่กับโหมดการกำหนดแอดเดรสบน Intel SnB ตระกูลซีพียู 3c สำหรับ 3 องค์ประกอบ ( [base+idx+const]ซึ่งใช้เวลาสองแยกเพิ่ม) แต่เพียง 1c กับ 2 หรือน้อยกว่าส่วนประกอบ (หนึ่งเพิ่ม) CPU บางตัว (เช่น Core2) ทำแม้แต่ 3 องค์ประกอบ LEA ในรอบเดียว แต่ SnB ตระกูลไม่ได้ ที่เลวร้ายยิ่งกว่าตระกูล Intel SnB- มาตรฐานเวลาแฝงดังนั้นจึงไม่มี 2c uopsมิฉะนั้น LEA 3 องค์ประกอบจะเป็นเพียง 2c เช่น Bulldozer (3 องค์ประกอบของ LEA นั้นช้ากว่าของ AMD เช่นกันไม่มากเท่า)

ดังนั้นlea rcx, [rax + rax*2]/ inc rcxเป็นเพียง 2c latency เร็วกว่าlea rcx, [rax + rax*2 + 1]บน Intel SnB ตระกูล CPU เช่น Haswell Break-even บน BD และแย่กว่าใน Core2 มันเสียค่าใช้จ่ายเพิ่ม uop ซึ่งโดยปกติแล้วจะไม่คุ้มค่าที่จะประหยัด 1c latency แต่ latency เป็นคอขวดที่สำคัญที่นี่และ Haswell มีท่อที่กว้างพอที่จะรองรับปริมาณงานที่เพิ่มขึ้นของ uop

ทั้ง gcc, icc และ clang (บน godbolt) ไม่ใช้เอาต์พุต CF ของ SHR โดยใช้ AND หรือ TESTเสมอ คอมไพเลอร์โง่ ๆ : P พวกมันเป็นชิ้นส่วนที่ซับซ้อนของเครื่องจักร แต่มนุษย์ที่ฉลาดสามารถเอาชนะพวกมันได้ในปัญหาเล็ก ๆ (ให้เวลานานกว่าพันถึงล้านเท่าในการคิดเกี่ยวกับมันคอมไพเลอร์ไม่ได้ใช้อัลกอริธึมที่ละเอียดถี่ถ้วนเพื่อค้นหาทุกวิธีที่เป็นไปได้ในการทำสิ่งต่าง ๆ เพราะมันจะใช้เวลานานเกินไปเมื่อปรับโค้ดอินไลน์จำนวนมาก พวกเขาทำได้ดีที่สุดพวกเขายังไม่จำลองแบบไปป์ไลน์ใน microar Architecture เป้าหมายอย่างน้อยก็ไม่ได้อยู่ในรายละเอียดเดียวกับIACAหรือเครื่องมือวิเคราะห์แบบคงที่อื่น ๆ พวกเขาใช้ฮิวริสติกบางอย่าง)


การวนรอบอย่างง่ายจะไม่ช่วยอะไร คอขวดห่วงนี้ในเวลาแฝงของห่วงโซ่พึ่งพาห่วงดำเนินการไม่ได้อยู่ในห่วงค่าใช้จ่าย / throughput ซึ่งหมายความว่ามันจะทำงานได้ดีกับการทำไฮเปอร์เธรด (หรือ SMT ชนิดอื่น) เนื่องจากซีพียูมีเวลามากมายในการแทรกคำแนะนำจากสองเธรด นี่จะหมายถึงการวนลูปแบบขนานmainแต่ก็ดีเพราะแต่ละเธรดสามารถตรวจสอบช่วงของnค่าและสร้างผลลัพธ์เป็นคู่ได้

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

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


คุณสามารถแม้กระทั่งทำเช่นนี้กับ SSE บรรจุ-เปรียบเทียบสิ่งที่จะเพิ่มเงื่อนไขเคาน์เตอร์สำหรับองค์ประกอบเวกเตอร์ที่nได้ไม่ถึง1เลย แล้วเพื่อซ่อนเวลาแฝงที่นานขึ้นของการใช้งานแบบเพิ่มเงื่อนไข SIMD คุณจะต้องเก็บnค่าเวกเตอร์ไว้ในอากาศมากขึ้น อาจคุ้มค่ากับ 256b เวกเตอร์ (4x uint64_t)

ฉันคิดว่ากลยุทธ์ที่ดีที่สุดในการตรวจจับ1"เหนียว" คือการปกปิดเวกเตอร์ของทุกคนที่คุณเพิ่มเพื่อเพิ่มเคาน์เตอร์ ดังนั้นหลังจากที่คุณเห็น a 1ในองค์ประกอบ, increment-vector จะมีศูนย์และ + = 0 คือ no-op

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

# starting with YMM0 = [ n_d, n_c, n_b, n_a ]  (64-bit elements)
# ymm4 = _mm256_set1_epi64x(1):  increment vector
# ymm5 = all-zeros:  count vector

.inner_loop:
    vpaddq    ymm1, ymm0, xmm0
    vpaddq    ymm1, ymm1, xmm0
    vpaddq    ymm1, ymm1, set1_epi64(1)     # ymm1= 3*n + 1.  Maybe could do this more efficiently?

    vprllq    ymm3, ymm0, 63                # shift bit 1 to the sign bit

    vpsrlq    ymm0, ymm0, 1                 # n /= 2

    # FP blend between integer insns may cost extra bypass latency, but integer blends don't have 1 bit controlling a whole qword.
    vpblendvpd ymm0, ymm0, ymm1, ymm3       # variable blend controlled by the sign bit of each 64-bit element.  I might have the source operands backwards, I always have to look this up.

    # ymm0 = updated n  in each element.

    vpcmpeqq ymm1, ymm0, set1_epi64(1)
    vpandn   ymm4, ymm1, ymm4         # zero out elements of ymm4 where the compare was true

    vpaddq   ymm5, ymm5, ymm4         # count++ in elements where n has never been == 1

    vptest   ymm4, ymm4
    jnz  .inner_loop
    # Fall through when all the n values have reached 1 at some point, and our increment vector is all-zero

    vextracti128 ymm0, ymm5, 1
    vpmaxq .... crap this doesn't exist
    # Actually just delay doing a horizontal max until the very very end.  But you need some way to record max and maxi.

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


การปรับปรุงอัลกอริทึม / การใช้งาน:

นอกเหนือจากการใช้ตรรกะเดียวกันกับ asm มีประสิทธิภาพมากขึ้นมองหาวิธีที่จะลดความซับซ้อนของตรรกะหรือหลีกเลี่ยงการทำงานซ้ำซ้อน เช่นบันทึกช่วยจำในการตรวจจับตอนจบทั่วไปของลำดับ หรือดียิ่งขึ้นดูที่ 8 บิตต่อท้ายในครั้งเดียว (คำตอบของ gnasher)

@EOF ชี้ให้เห็นว่าtzcnt(หรือbsf) สามารถใช้ในการทำn/=2ซ้ำหลายรายการในขั้นตอนเดียว นั่นอาจจะดีกว่า SIMD เวกเตอร์ คำสั่ง SSE หรือ AVX ไม่สามารถทำได้ มันยังคงเข้ากันได้กับการทำหลายสเกลาร์nแบบขนานในการลงทะเบียนจำนวนเต็มที่แตกต่างกัน

ดังนั้นลูปอาจมีลักษณะเช่นนี้:

goto loop_entry;  // C++ structured like the asm, for illustration only
do {
   n = n*3 + 1;
  loop_entry:
   shift = _tzcnt_u64(n);
   n >>= shift;
   count += shift;
} while(n != 1);

สิ่งนี้อาจทำซ้ำได้น้อยลงอย่างมาก แต่การกะนับจำนวนตัวแปรช้าสำหรับซีพียู Intel SnB ตระกูลที่ไม่มี BMI2 3 uops, 2c latency (พวกเขามีการพึ่งพาการป้อนข้อมูลใน FLAGS เพราะ count = 0 หมายถึงธงที่ไม่ได้แก้ไขพวกเขาจัดการสิ่งนี้เป็นการพึ่งพาข้อมูลและใช้หลาย uops เพราะ uop สามารถมี 2 อินพุตเท่านั้น (pre-HSW / BDW ต่อไป)) นี่เป็นประเภทที่ผู้คนร้องเรียนเกี่ยวกับการออกแบบบ้า -CISC ของ x86 ที่อ้างถึง มันทำให้ซีพียู x86 ช้ากว่าที่ควรจะเป็นหาก ISA ได้รับการออกแบบตั้งแต่เริ่มต้นจนถึงทุกวันนี้แม้จะคล้ายกันมาก (กล่าวคือนี่เป็นส่วนหนึ่งของ "ภาษี x86" ที่มีค่าความเร็ว / พลังงาน) SHRX / SHLX / SARX (BMI2) ถือเป็นชัยชนะครั้งใหญ่ (1 uop / 1c latency)

นอกจากนี้ยังวาง tzcnt (3c บน Haswell และใหม่กว่า) บนเส้นทางคริติคอลดังนั้นจึงมีความหมายรวมถึงความหน่วงแฝงทั้งหมดของห่วงโซ่การพึ่งพาแบบวนรอบ มันไม่จำเป็นต้องลบสำหรับ CMOV หรือสำหรับการเตรียมการลงทะเบียนถือครองn>>1ใด ๆ @ คำตอบของ Veedrac จะเอาชนะทุกสิ่งนี้โดยการเลื่อน tzcnt / shift สำหรับการวนซ้ำหลายครั้งซึ่งมีประสิทธิภาพสูง (ดูด้านล่าง)

เราสามารถใช้BSFหรือTZCNTอย่างปลอดภัยแทนกันได้เนื่องจากnไม่สามารถเป็นศูนย์ ณ จุดนั้น รหัสเครื่องของ TZCNT ถอดรหัสเป็น BSF บน CPU ที่ไม่รองรับ BMI1 (ส่วนนำหน้าที่ไม่มีความหมายจะถูกข้ามดังนั้น REP BSF จึงทำงานเหมือน BSF)

TZCNT ทำงานได้ดีกว่า BSF บน CPU AMD ที่รองรับดังนั้นจึงเป็นความคิดที่ดีที่จะใช้REP BSFแม้ว่าคุณจะไม่สนใจการตั้งค่า ZF หากอินพุตเป็นศูนย์แทนที่จะเป็นเอาต์พุต คอมไพเลอร์บางคนทำเช่นนี้เมื่อคุณใช้แม้จะมี __builtin_ctzll-mno-bmi

พวกมันทำงานบน CPU ของ Intel เหมือนกันดังนั้นเพียงบันทึกไบต์ถ้านั่นคือสิ่งที่สำคัญ TZCNT บน Intel (pre-Skylake) ยังคงมีการพึ่งพาเท็จในตัวถูกดำเนินการเอาต์พุตแบบเขียนอย่างเดียวที่ควรจะเป็นเช่นเดียวกับ BSF เพื่อสนับสนุนพฤติกรรมที่ไม่มีเอกสารที่ BSF ที่มีอินพุต = 0 ทำให้ปลายทางไม่ถูกแก้ไข ดังนั้นคุณต้องหลีกเลี่ยงสิ่งนั้นเว้นแต่ว่าจะปรับให้เหมาะสมสำหรับ Skylake เท่านั้นดังนั้นจึงไม่มีอะไรที่จะได้รับจาก REP พิเศษ (Intel มักจะไปข้างต้นและนอกเหนือสิ่งที่คู่มือ x86 ISA ต้องเพื่อหลีกเลี่ยงการทำลายรหัสใช้กันอย่างแพร่หลายที่ขึ้นอยู่กับบางสิ่งบางอย่างมันไม่ควรหรือที่จะไม่อนุญาตให้มีผลย้อนหลัง. เช่นWindows 9x ของถือว่าไม่มีการโหลดล่วงหน้าเก็งกำไรของรายการ TLBซึ่งเป็นที่ปลอดภัย เมื่อรหัสถูกเขียนขึ้นก่อนที่ Intel จะอัพเดทกฎการจัดการ TLB )

อย่างไรก็ตาม LZCNT / TZCNT บน Haswell มี dep ที่ผิดพลาดเหมือนกับ POPCNT: ดูคำถาม & คำตอบนี้ นี่คือสาเหตุที่เอาต์พุต gsm ของ ascc สำหรับโค้ดของ @ Veedrac คุณเห็นว่าการทำลายโซ่ dep ด้วย xor-zeroingบน register มันกำลังจะใช้เป็นปลายทางของ TZCNT เมื่อไม่ใช้ dst = src เนื่องจาก TZCNT / LZCNT / POPCNT ไม่เคยออกจากปลายทางที่ไม่ได้กำหนดหรือไม่ได้แก้ไขดังนั้นการพึ่งพาเท็จในผลลัพธ์บน CPU ของ Intel จึงเป็นข้อบกพร่อง / ข้อ จำกัด ด้านประสิทธิภาพ สันนิษฐานว่ามันคุ้มค่ากับทรานซิสเตอร์ / พลังงานที่ให้พวกมันทำงานเหมือน uops อื่น ๆ ที่ไปยังหน่วยปฏิบัติการเดียวกัน ข้อเสียที่สมบูรณ์แบบเพียงอย่างเดียวคือการโต้ตอบกับข้อ จำกัด uarch อื่น: พวกเขาสามารถ micro-ฟิวส์ตัวถูกดำเนินการหน่วยความจำด้วยโหมดที่อยู่ดัชนี บน Haswell แต่บน Skylake ที่ Intel ลบ dep ที่ผิดสำหรับ LZCNT / TZCNT พวกเขาจะ "ยกเลิกการลามิเนต" โหมดการทำดัชนีที่อยู่ในขณะที่ POPCNT ยังคงสามารถไมโครโหมด addr ใด ๆ


การปรับปรุงแนวคิด / รหัสจากคำตอบอื่น ๆ :

คำตอบของ @ hidefromkgbมีข้อสังเกตที่ดีว่าคุณรับประกันได้ว่าจะสามารถทำการเปลี่ยนแปลงได้อย่างถูกต้องหลังจาก 3n + 1 คุณสามารถคำนวณได้อย่างมีประสิทธิภาพมากขึ้นกว่าเพียงแค่ออกจากการตรวจสอบระหว่างขั้นตอน การใช้ asm ในคำตอบนั้นแตก แต่ (ขึ้นอยู่กับของซึ่งไม่ได้กำหนดหลังจาก SHRD ด้วยจำนวน> 1) และช้า: ROR rdi,2เร็วกว่าSHRD rdi,rdi,2และใช้คำสั่ง CMOV สองคำสั่งบนเส้นทางวิกฤตินั้นช้ากว่าการทดสอบพิเศษ ที่สามารถทำงานแบบขนาน

ฉันใส่ tidied / C ที่ดีขึ้น (ซึ่งแนะนำคอมไพเลอร์ในการผลิต asm ดีกว่า) และการทดสอบการทำงาน + asm เร็วขึ้น (ในความคิดเห็นด้านล่างซี) ขึ้นบน Godbolt: เห็นการเชื่อมโยงในคำตอบ @ hidefromkgb ของ (คำตอบนี้ใช้ขีด จำกัด ถ่าน 30k จาก Godbolt URL ที่มีขนาดใหญ่ แต่Shortlink สามารถเน่าและยาวเกินไปสำหรับ goo.gl ต่อไป)

ปรับปรุงการพิมพ์เอาต์พุตเพื่อแปลงเป็นสตริงและสร้างwrite()แทนการเขียนทีละตัวอักษร สิ่งนี้จะลดผลกระทบต่อการกำหนดเวลาโปรแกรมทั้งหมดด้วยperf stat ./collatz(เพื่อบันทึกเคาน์เตอร์วัดประสิทธิภาพ) และฉันไม่สับสนกับ asm ที่ไม่สำคัญ


@ รหัสของ Veedrac

ฉันได้รับความเร็วเล็กน้อยจากการเลื่อนขวาเท่าที่เรารู้ว่าต้องทำและการตรวจสอบเพื่อวนรอบต่อไป จาก 7.5 วินาทีสำหรับ limit = 1e8 ลงไปที่ 7.275s บน Core2Duo (Merom) โดยมีปัจจัยที่ไม่สามารถเลื่อนได้ที่ 16

รหัสเมือง + ความคิดเห็นเกี่ยวกับ Godbolt อย่าใช้เวอร์ชั่นนี้กับเสียงดังกราว มันทำอะไรโง่ ๆ กับ defer-loop การใช้ตัวนับ tmp kแล้วเพิ่มเข้าไปในcountภายหลังจะเปลี่ยนสิ่งที่เสียงดังกราวทำ แต่สิ่งนี้ทำให้เจ็บเล็กน้อย

ดูการสนทนาในความคิดเห็น: รหัสของ Veedrac นั้นยอดเยี่ยมสำหรับซีพียูที่มี BMI1 (เช่นไม่ใช่ Celeron / Pentium)


4
ฉันลองใช้วิธีการแบบเวกเตอร์สักพักแล้วมันไม่ได้ช่วยอะไร (เพราะคุณสามารถทำได้ดีกว่าในรหัสสเกลาร์ด้วยtzcntและคุณถูกล็อคให้อยู่ในลำดับที่ยาวที่สุดในบรรดาองค์ประกอบแบบเวกเตอร์ของคุณในกรณีแบบเวกเตอร์)
EOF

3
@EOF: ไม่มีฉันหมายถึงการทำลายออกจากวงภายในเมื่อใดคนหนึ่งของความนิยมองค์ประกอบเวกเตอร์1แทนเมื่อพวกเขาทั้งหมดมี (ที่ตรวจพบได้อย่างง่ายดายด้วย PCMPEQ / PMOVMSK) จากนั้นคุณใช้ PINSRQ และสิ่งของเพื่อทำซอกับองค์ประกอบหนึ่งที่ถูกยกเลิก (และเคาน์เตอร์) และกระโดดกลับเข้าไปในลูป นั่นอาจกลายเป็นความสูญเสียได้ง่ายเมื่อคุณแยกวงในบ่อยเกินไป แต่นั่นหมายความว่าคุณจะได้งานที่มีประโยชน์ 2 หรือ 4 องค์ประกอบทำซ้ำทุกครั้งของวงใน จุดดีเกี่ยวกับการบันทึกความจำแม้ว่า
Peter Cordes

4
@jefferson ดีที่สุดที่ฉันจัดการจะgodbolt.org/g/1N70Ib ฉันหวังว่าฉันจะทำสิ่งที่ฉลาดกว่า แต่ดูเหมือนจะไม่
Veedrac

87
สิ่งที่ทำให้ฉันประหลาดใจเกี่ยวกับคำตอบที่น่าเหลือเชื่อเช่นนี้คือความรู้ที่แสดงให้เห็นรายละเอียดดังกล่าว ฉันจะไม่มีวันรู้ภาษาหรือระบบในระดับนั้นและฉันก็ไม่รู้เหมือนกัน ท่านทำได้ดีมาก
camden_kid

8
คำตอบในตำนาน !!
Sumit Jain

104

การอ้างว่าคอมไพเลอร์ C ++ สามารถสร้างโค้ดที่ดีที่สุดได้มากกว่าโปรแกรมเมอร์ภาษาแอสเซมบลีที่มีความสามารถเป็นข้อผิดพลาดที่แย่มาก และโดยเฉพาะในกรณีนี้ มนุษย์สามารถทำให้โค้ดดีขึ้นได้เมื่อคอมไพเลอร์สามารถทำได้และสถานการณ์เฉพาะนี้เป็นตัวอย่างที่ดีของการอ้างสิทธิ์นี้

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

(รหัสด้านล่างคือ 32 บิต แต่สามารถแปลงเป็น 64 บิตได้อย่างง่ายดาย)

ตัวอย่างเช่นฟังก์ชั่นลำดับสามารถปรับให้เหมาะสมเพียง 5 คำแนะนำ:

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

รหัสทั้งหมดดูเหมือนว่า:

include "%lib%/freshlib.inc"
@BinaryType console, compact
options.DebugMode = 1
include "%lib%/freshlib.asm"

start:
        InitializeAll
        mov ecx, 999999
        xor edi, edi        ; max
        xor ebx, ebx        ; max i

    .main_loop:

        xor     esi, esi
        mov     eax, ecx

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

        cmp     edi, esi
        cmovb   edi, esi
        cmovb   ebx, ecx

        dec     ecx
        jnz     .main_loop

        OutputValue "Max sequence: ", edi, 10, -1
        OutputValue "Max index: ", ebx, 10, -1

        FinalizeAll
        stdcall TerminateAll, 0

เพื่อรวบรวมรหัสนี้FreshLibเป็นสิ่งจำเป็น

ในการทดสอบของฉัน (โปรเซสเซอร์ 1 GHz AMD A4-1200) โค้ดด้านบนนั้นเร็วกว่ารหัส C ++ ประมาณสี่เท่าจากคำถาม (เมื่อรวบรวมด้วย-O0: 430 ms เทียบกับ 1900 ms) และเร็วกว่าสองเท่า (430 มิลลิวินาทีเทียบกับ 830 มิลลิวินาที) เมื่อ c ++ -O3รหัสจะรวบรวมกับ

ผลลัพธ์ของทั้งสองโปรแกรมเหมือนกัน: max sequence = 525 on i = 837799


6
นั่นมันฉลาดดี SHR ตั้งค่า ZF เฉพาะเมื่อ EAX เท่ากับ 1 (หรือ 0) ฉันพลาดว่าเมื่อปรับการแสดง-O3ผลของ gcc ให้เหมาะสม แต่ฉันเห็นการเพิ่มประสิทธิภาพอื่น ๆ ทั้งหมดที่คุณทำกับลูปด้านใน (แต่ทำไมคุณถึงใช้ LEA สำหรับการเพิ่มตัวนับแทนที่จะเป็น INC มันโอเคที่จะปิดบังการตั้งค่าสถานะ ณ จุดนั้นและนำไปสู่การชะลอตัวในสิ่งใดก็ตามยกเว้น P4 (การพึ่งพาเท็จบนแฟล็กเก่าสำหรับทั้ง INC และ SHR) LEA สามารถ ' เสื้อทำงานบนพอร์ตจำนวนมากและอาจนำไปสู่ความขัดแย้งทรัพยากรล่าช้าในเส้นทางที่สำคัญมักจะมากกว่า).
ปีเตอร์ Cordes

4
โอ้จริงแล้ว Bulldozer อาจมีปัญหาคอขวดในปริมาณงานที่มีคอมไพเลอร์เอาท์พุท มันมี CMOV latency ต่ำและ LEA 3 องค์ประกอบมากกว่า Haswell (ซึ่งฉันกำลังพิจารณา) ดังนั้น chain dep-loop ที่นำมาใช้จึงเป็นเพียง 3 รอบในโค้ดของคุณ นอกจากนี้ยังไม่มีคำแนะนำ MOV แบบ zero-latency สำหรับการลงทะเบียนจำนวนเต็มดังนั้นคำสั่ง MOV ที่สูญเปล่าไปแล้วของ g ++ จะเพิ่มเวลาแฝงของเส้นทางวิกฤติและเป็นเรื่องใหญ่สำหรับ Bulldozer ดังนั้นการเพิ่มประสิทธิภาพด้วยมือจริง ๆ จะเอาชนะคอมไพเลอร์ในวิธีการที่สำคัญสำหรับซีพียูที่ไม่ทันสมัยพอที่จะเคี้ยวผ่านคำแนะนำที่ไร้ประโยชน์
Peter Cordes

95
"การอ้างว่าคอมไพเลอร์ C ++ ดีกว่านั้นเป็นความผิดพลาดที่แย่มากและโดยเฉพาะอย่างยิ่งในกรณีนี้มนุษย์สามารถสร้างรหัสได้ดีกว่าเสมอและปัญหานี้เป็นตัวอย่างที่ดีของการอ้างสิทธิ์นี้ " คุณสามารถย้อนกลับได้ . "การอ้างว่ามนุษย์ดีกว่านั้นคือความผิดพลาดที่แย่มากและโดยเฉพาะอย่างยิ่งในกรณีนี้มนุษย์สามารถสร้างรหัสได้แย่กว่านั้นและคำถามนี้เป็นตัวอย่างที่ดีของการอ้างสิทธิ์นี้ " ดังนั้นฉันไม่คิดว่าคุณจะมีประเด็นที่นี่ การสรุปเช่นนั้นผิด
luk32

5
@ luk32 - แต่ผู้เขียนคำถามไม่สามารถโต้แย้งใด ๆ เลยเพราะความรู้ภาษาประกอบของเขาอยู่ใกล้กับศูนย์ ทุกข้อโต้แย้งเกี่ยวกับมนุษย์กับคอมไพเลอร์โดยปริยายถือว่าเป็นมนุษย์อย่างน้อยก็มีความรู้ระดับกลางอย่าง asm เพิ่มเติม: ทฤษฎีบท "รหัสที่มนุษย์เขียนขึ้นจะดีกว่าหรือเหมือนกับรหัสที่สร้างจากคอมไพเลอร์" นั้นง่ายต่อการพิสูจน์อย่างเป็นทางการ
johnfound

30
@ luk32: มนุษย์ที่มีทักษะสามารถ (และโดยปกติควร) เริ่มต้นด้วยคอมไพเลอร์เอาท์พุท ดังนั้นตราบใดที่คุณเปรียบเทียบความพยายามของคุณเพื่อให้แน่ใจว่าพวกเขากำลังเร็วขึ้นจริง ๆ (บนฮาร์ดแวร์เป้าหมายที่คุณปรับแต่ง) คุณจะไม่สามารถทำได้แย่กว่าคอมไพเลอร์ แต่ใช่ฉันต้องยอมรับว่ามันเป็นประโยคที่แข็งแกร่ง คอมไพเลอร์มักจะทำดีกว่า coders asm สามเณร แต่มักจะเป็นไปได้ที่จะบันทึกคำสั่งหรือสองคำสั่งเมื่อเทียบกับสิ่งที่คอมไพเลอร์เกิดขึ้น (ไม่เสมอไปในเส้นทางที่สำคัญขึ้นอยู่กับ uarch) มันเป็นชิ้นส่วนที่ซับซ้อนของเครื่องจักรที่มีประโยชน์สูง แต่มันไม่ใช่ "ฉลาด"
Peter Cordes

24

เพื่อประสิทธิภาพที่มากขึ้น: การเปลี่ยนแปลงอย่างง่าย ๆ กำลังสังเกตว่าหลังจาก n = 3n + 1, n จะเท่ากันดังนั้นคุณสามารถหารด้วย 2 ได้ทันที และจะไม่เป็น 1 ดังนั้นคุณไม่จำเป็นต้องทดสอบ ดังนั้นคุณสามารถบันทึกบางอย่างถ้างบและเขียน:

while (n % 2 == 0) n /= 2;
if (n > 1) for (;;) {
    n = (3*n + 1) / 2;
    if (n % 2 == 0) {
        do n /= 2; while (n % 2 == 0);
        if (n == 1) break;
    }
}

นี่คือชัยชนะครั้งยิ่งใหญ่ : ถ้าคุณดูที่ 8 บิตต่ำสุดของ n ขั้นตอนทั้งหมดจนกว่าคุณจะหารด้วย 2 แปดครั้งจะถูกกำหนดโดยแปดบิตเหล่านั้นอย่างสมบูรณ์ ตัวอย่างเช่นหากแปดบิตสุดท้ายเป็น 0x01 นั่นคือในเลขฐานสองของคุณคือ ???? 0000 0001 จากนั้นขั้นตอนต่อไปคือ:

3n+1 -> ???? 0000 0100
/ 2  -> ???? ?000 0010
/ 2  -> ???? ??00 0001
3n+1 -> ???? ??00 0100
/ 2  -> ???? ???0 0010
/ 2  -> ???? ???? 0001
3n+1 -> ???? ???? 0100
/ 2  -> ???? ???? ?010
/ 2  -> ???? ???? ??01
3n+1 -> ???? ???? ??00
/ 2  -> ???? ???? ???0
/ 2  -> ???? ???? ????

ดังนั้นขั้นตอนเหล่านี้สามารถคาดการณ์ได้และ 256k +1 จะถูกแทนที่ด้วย 81k + 1 สิ่งที่คล้ายกันจะเกิดขึ้นสำหรับชุดค่าผสมทั้งหมด ดังนั้นคุณสามารถสร้าง loop โดยใช้คำสั่ง switch ขนาดใหญ่:

k = n / 256;
m = n % 256;

switch (m) {
    case 0: n = 1 * k + 0; break;
    case 1: n = 81 * k + 1; break; 
    case 2: n = 81 * k + 1; break; 
    ...
    case 155: n = 729 * k + 425; break;
    ...
}

วิ่งวนซ้ำจนกระทั่ง n ≤ 128 เพราะ ณ จุดนั้น n อาจกลายเป็น 1 โดยมีหน่วยน้อยกว่าแปดโดย 2 และทำขั้นตอนแปดครั้งขึ้นไปจะทำให้คุณพลาดจุดที่คุณไปถึง 1 เป็นครั้งแรก จากนั้นดำเนินการวนรอบ "ปกติ" ต่อไปหรือเตรียมตารางที่จะบอกคุณว่าต้องมีอีกหลายขั้นตอนในการเข้าถึง 1

PS ฉันสงสัยอย่างยิ่งว่าคำแนะนำของ Peter Cordes จะทำให้เร็วขึ้น จะไม่มีสาขาที่มีเงื่อนไขเลยยกเว้นสาขาใดสาขาหนึ่งและสาขานั้นจะคาดเดาได้อย่างถูกต้องยกเว้นเมื่อลูปสิ้นสุดลงจริง ดังนั้นรหัสจะเป็นอย่างไร

static const unsigned int multipliers [256] = { ... }
static const unsigned int adders [256] = { ... }

while (n > 128) {
    size_t lastBits = n % 256;
    n = (n >> 8) * multipliers [lastBits] + adders [lastBits];
}

ในทางปฏิบัติคุณจะวัดว่าการประมวลผลครั้งสุดท้ายที่ 9, 10, 11, 12 บิตของ n จะเร็วขึ้นหรือไม่ สำหรับแต่ละบิตจำนวนรายการในตารางจะเพิ่มขึ้นเป็นสองเท่าและฉันจะลดการชะลอตัวลงเมื่อตารางไม่พอดีกับแคช L1 อีกต่อไป

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

เราสามารถกำหนดปัญหาใหม่ได้เล็กน้อย: แทนที่ n ด้วย (3n + 1) / 2 ถ้าแปลกและแทนที่ n ด้วย n / 2 ถ้าเท่ากัน จากนั้นทุก ๆ การวนซ้ำจะทำตามขั้นตอน 8 ขั้น แต่คุณสามารถพิจารณาว่าการโกง :-) ดังนั้นสมมติว่ามีการดำเนินการ r n <- 3n + 1 และการดำเนินการ s n <- n / 2 ผลลัพธ์จะค่อนข้าง n '= n * 3 ^ r / 2 ^ s เนื่องจาก n <- 3n + 1 หมายถึง n <- 3n * (1 + 1 / 3n) การหาลอการิทึมเราพบ r = (s + log2 (n '/ n)) / log2 (3)

หากเราวนซ้ำจนถึง n ≤ 1,000,000 และมีตารางที่คำนวณล่วงหน้าจำนวนการทำซ้ำจำเป็นต้องใช้จากจุดเริ่มต้นใด ๆ n 1,000,000 จากนั้นคำนวณ r ข้างต้นปัดเศษเป็นจำนวนเต็มที่ใกล้เคียงที่สุดจะให้ผลลัพธ์ที่ถูกต้องเว้นแต่ s มีขนาดใหญ่


2
หรือสร้างตารางการค้นหาข้อมูลสำหรับการคูณและเพิ่มค่าคงที่แทนสวิตช์ การทำดัชนีตาราง 256 รายการสองรายการนั้นเร็วกว่าตารางกระโดดและคอมไพเลอร์อาจไม่ต้องการค้นหาการแปลงนั้น
Peter Cordes

1
อืมฉันคิดว่าการสังเกตุนี้เป็นเวลาหนึ่งนาทีอาจพิสูจน์การคาดคะเนของโคลลาตซ์ แต่ไม่แน่นอน สำหรับทุก ๆ บิตที่เป็นไปได้ 8 บิตจะมีจำนวนขั้นตอน จำกัด จนกว่าจะหมด แต่รูปแบบ 8 บิตต่อท้ายเหล่านี้บางส่วนจะยืดส่วนที่เหลือของบิตสตริงได้มากกว่า 8 ดังนั้นจึงไม่สามารถตัดการเติบโตที่ไม่ได้ จำกัด หรือรอบการทำซ้ำได้
Peter Cordes

ในการอัปเดตcountคุณต้องมีอาร์เรย์ที่สามใช่ไหม adders[]ไม่ได้บอกคุณว่ามีการเปลี่ยนแปลงด้านขวากี่ครั้ง
Peter Cordes

สำหรับตารางที่มีขนาดใหญ่กว่าจะคุ้มค่าหากใช้ชนิดที่แคบกว่าเพื่อเพิ่มความหนาแน่นของแคช ในสถาปัตยกรรมส่วนใหญ่โหลดที่ไม่มีการขยายจาก a uint16_tจะถูกมาก บน x86 ก็แค่ถูกเป็นศูนย์ขยายจาก 32 บิตเพื่อunsigned int uint64_t(MOVZX จากหน่วยความจำบน CPU ของ Intel เท่านั้นต้องการ UOP โหลดพอร์ต แต่ AMD ซีพียูไม่ต้อง ALU เช่นกัน.) โอ้ BTW ทำไมคุณใช้size_tสำหรับlastBits? มันเป็นประเภท 32 บิตด้วย-m32และแม้กระทั่ง-mx32(โหมดยาวที่มีตัวชี้ 32 บิต) nมันแน่นอนผิดประเภทสำหรับ unsignedใช้เพียงแค่
Peter Cordes

20

ในหมายเหตุที่ค่อนข้างไม่เกี่ยวข้อง: มีประสิทธิภาพมากขึ้น!

  • [การคาดเดา«ครั้งแรก»ถูก debunked ในที่สุดโดย @ShreevatsaR; ลบ]

  • เมื่อเข้าไปในลำดับเราจะได้รับ 3 กรณีที่เป็นไปได้ใน 2-Neighborhood ขององค์ประกอบปัจจุบันN(แสดงก่อน):

    1. [คู่] [แปลก]
    2. [คี่] [คู่]
    3. [คู่] [แม้]

    จะกระโดดที่ผ่านมา 2 องค์ประกอบเหล่านี้หมายถึงการคำนวณ(N >> 1) + N + 1, ((N << 1) + N + 1) >> 1และN >> 2ตามลำดับ

    พิสูจน์ว่าทั้งสองกรณี (1) และ (2) เป็นไปได้ที่จะใช้สูตรแรก, (N >> 1) + N + 1.

    กรณี (1) ชัดเจน กรณี (2) หมายถึง(N & 1) == 1ดังนั้นถ้าเราคิดว่า (โดยไม่สูญเสียความเป็นไปได้ทั่วไป) ว่า N นั้นมีความยาว 2 บิตและบิตของมันbaมาจากส่วนใหญ่ถึงความสำคัญน้อยที่สุดจากนั้นa = 1และการรักษาต่อไปนี้:

    (N << 1) + N + 1:     (N >> 1) + N + 1:
    
            b10                    b1
             b1                     b
           +  1                   + 1
           ----                   ---
           bBb0                   bBb

    B = !bที่ไหน การเปลี่ยนผลลัพธ์แรกให้ถูกต้องคือสิ่งที่เราต้องการ

    (N & 1) == 1 ⇒ (N >> 1) + N + 1 == ((N << 1) + N + 1) >> 1QED:

    จากการพิสูจน์แล้วเราสามารถสำรวจองค์ประกอบลำดับที่ 2 ในแต่ละครั้งโดยใช้การดำเนินการแบบไตรภาคเดี่ยว ลดเวลาอีก 2 ×

อัลกอริทึมที่ได้จะมีลักษณะดังนี้:

uint64_t sequence(uint64_t size, uint64_t *path) {
    uint64_t n, i, c, maxi = 0, maxc = 0;

    for (n = i = (size - 1) | 1; i > 2; n = i -= 2) {
        c = 2;
        while ((n = ((n & 3)? (n >> 1) + n + 1 : (n >> 2))) > 2)
            c += 2;
        if (n == 2)
            c++;
        if (c > maxc) {
            maxi = i;
            maxc = c;
        }
    }
    *path = maxc;
    return maxi;
}

int main() {
    uint64_t maxi, maxc;

    maxi = sequence(1000000, &maxc);
    printf("%llu, %llu\n", maxi, maxc);
    return 0;
}

ที่นี่เราเปรียบเทียบn > 2เพราะกระบวนการอาจหยุดที่ 2 แทน 1 หากความยาวทั้งหมดของลำดับนั้นเป็นเลขคี่

[แก้ไข:]

มาแปลสิ่งนี้เป็นชุดประกอบ!

MOV RCX, 1000000;



DEC RCX;
AND RCX, -2;
XOR RAX, RAX;
MOV RBX, RAX;

@main:
  XOR RSI, RSI;
  LEA RDI, [RCX + 1];

  @loop:
    ADD RSI, 2;
    LEA RDX, [RDI + RDI*2 + 2];
    SHR RDX, 1;
    SHRD RDI, RDI, 2;    ror rdi,2   would do the same thing
    CMOVL RDI, RDX;      Note that SHRD leaves OF = undefined with count>1, and this doesn't work on all CPUs.
    CMOVS RDI, RDX;
    CMP RDI, 2;
  JA @loop;

  LEA RDX, [RSI + 1];
  CMOVE RSI, RDX;

  CMP RAX, RSI;
  CMOVB RAX, RSI;
  CMOVB RBX, RCX;

  SUB RCX, 2;
JA @main;



MOV RDI, RCX;
ADD RCX, 10;
PUSH RDI;
PUSH RCX;

@itoa:
  XOR RDX, RDX;
  DIV RCX;
  ADD RDX, '0';
  PUSH RDX;
  TEST RAX, RAX;
JNE @itoa;

  PUSH RCX;
  LEA RAX, [RBX + 1];
  TEST RBX, RBX;
  MOV RBX, RDI;
JNE @itoa;

POP RCX;
INC RDI;
MOV RDX, RDI;

@outp:
  MOV RSI, RSP;
  MOV RAX, RDI;
  SYSCALL;
  POP RAX;
  TEST RAX, RAX;
JNE @outp;

LEA RAX, [RDI + 59];
DEC RDI;
SYSCALL;

ใช้คำสั่งเหล่านี้เพื่อรวบรวม:

nasm -f elf64 file.asm
ld -o file file.o

ดู C และเวอร์ชัน asm โดย Peter Cordes บน Godbolt ที่ได้รับการปรับปรุง / แก้ไขข้อผิดพลาด (หมายเหตุบรรณาธิการ: ขออภัยที่ใส่ข้อมูลของฉันในคำตอบของคุณ แต่คำตอบของฉันถึงขีด จำกัด ถ่าน 30k จากลิงค์ Godbolt + ข้อความ!)


2
ไม่มีหนึ่งดังกล่าวว่าQ 12 = 3Q + 1ประเด็นแรกของคุณไม่ถูกต้อง
Veedrac

1
@Veedrac: เล่นกับสิ่งนี้: มันสามารถนำไปใช้กับ asm ได้ดีกว่าการนำไปใช้ในคำตอบนี้โดยใช้ ROR / TEST และ CMOV เพียงอันเดียว รหัส asm นี้ไม่มีที่สิ้นสุด - ลูปบน CPU ของฉันเนื่องจากเห็นได้ชัดว่าอาศัย OF ซึ่งไม่ได้กำหนดหลังจาก SHRD หรือ ROR ด้วยจำนวน> 1 นอกจากนี้ยังมีความพยายามอย่างมากในการหลีกเลี่ยงmov reg, imm32และบันทึกไบต์ แต่จากนั้นจะใช้ แม้แต่รีจิสเตอร์เวอร์ชัน 64 บิตทุกที่xor rax, raxดังนั้นมันจึงมีคำนำหน้า REX ที่ไม่จำเป็นมากมาย เห็นได้ชัดว่าเราต้องการเพียง REX ในการถือครองnในวงในเพื่อหลีกเลี่ยงการล้น
Peter Cordes

1
ผลการ Timing (จาก Core2Duo E6600:. 2.4GHz Merom คอมเพล็กซ์ LEA = 1c แฝง CMOV = 2c) การใช้งาน Inner-loop asm แบบขั้นตอนเดียวที่ดีที่สุด (จาก Johnfound): 111ms ต่อการรันของ @main loop นี้ คอมไพเลอร์เอาท์พุทจากเวอร์ชันที่ไม่ทำให้ยุ่งเหยิงของฉันของ C นี้ (ที่มี tmp vars): clang3.8 -O3 -march=core2: 96ms gcc5.2: 108ms จากลูปด้านในของฉันที่พัฒนาแล้วของ clang: 92ms (ควรเห็นการปรับปรุงที่ยิ่งใหญ่กว่าในตระกูล SnB ซึ่ง LEA ที่ซับซ้อนคือ 3c ไม่ใช่ 1c) จากการปรับปรุง + เวอร์ชันการทำงานของลูป asm นี้ของฉัน (โดยใช้ ROR + TEST ไม่ใช่ SHRD): 87ms วัดด้วย 5 reps ก่อนการพิมพ์
Peter Cordes

2
ต่อไปนี้เป็น 66 ตัวตั้งค่าระเบียนแรก (A006877 บน OEIS) ฉันได้ทำเครื่องหมายตัวหนาคู่: 2, 3, 6, 7, 9, 18, 25, 27, 54, 73, 97, 129, 171, 231, 313, 327, 649, 703, 871, 1161, 2223, 2463, 2919, 3711, 6171, 10971, 13255, 17647, 23529, 26623, 34239, 35655, 52527, 77031, 106239, 142587, 156159, 230631, 230631, 410011, 511935, 626331 1723519, 2298025, 3064033, 3542887, 3732423, 5649499, 6649279, 8400511, 11200681, 14934241, 15733191, 31466382, 36791535, 63728127, 127456254, 169941673, 226588897, 268549803, 537099606, 670617279, 1341234558
ShreevatsaR

1
@hidefromkgb เยี่ยมมาก! และฉันขอขอบคุณจุดอื่น ๆ ของคุณดีขึ้นเช่นกัน: 4k + 2 → 2k + 1 → 6k + 4 = (4k + 2) + (2k + 1) + 1 และ 2k + 1 → 6k + 4 → 3k + 2 = ( 2k + 1) + (k) + 1. การสังเกตดีมาก!
ShreevatsaR

6

โปรแกรม C ++ ได้รับการแปลเป็นโปรแกรมการประกอบระหว่างการสร้างรหัสเครื่องจากซอร์สโค้ด มันผิดที่จะบอกว่าชุดประกอบช้ากว่า C ++ ยิ่งไปกว่านั้นรหัสไบนารีที่สร้างขึ้นนั้นแตกต่างจากคอมไพเลอร์ถึงคอมไพเลอร์ ดังนั้นคอมไพเลอร์สมาร์ท C ++ อาจสร้างรหัสไบนารี่ที่เหมาะสมและมีประสิทธิภาพมากกว่าโค้ดของแอสเซมเบลอร์โง่

อย่างไรก็ตามฉันเชื่อว่าวิธีการทำโปรไฟล์ของคุณมีข้อบกพร่องบางอย่าง ต่อไปนี้เป็นแนวทางทั่วไปสำหรับการทำโปรไฟล์:

  1. ตรวจสอบให้แน่ใจว่าระบบของคุณอยู่ในสถานะปกติ / ไม่ได้ใช้งาน หยุดกระบวนการที่กำลังทำงานอยู่ทั้งหมด (แอปพลิเคชัน) ที่คุณเริ่มต้นหรือที่ใช้ CPU อย่างเข้มข้น (หรือโพลบนเครือข่าย)
  2. ข้อมูลของคุณต้องมีขนาดใหญ่กว่า
  3. การทดสอบของคุณต้องใช้เวลานานกว่า 5-10 วินาที
  4. อย่าพึ่งตัวอย่างเพียงอันเดียว ทำแบบทดสอบ N ครั้ง รวบรวมผลลัพธ์และคำนวณค่าเฉลี่ยหรือค่ามัธยฐานของผลลัพธ์

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

9
อาจไม่ใช่แค่ข้อผิดพลาดในการวัดรหัส asm ที่เขียนด้วยมือกำลังใช้คำสั่ง DIV 64 บิตแทนการเลื่อนขวา ดูคำตอบของฉัน แต่ใช่การวัดอย่างถูกต้องก็มีความสำคัญเช่นกัน
Peter Cordes

7
สัญลักษณ์แสดงหัวข้อย่อยมีการจัดรูปแบบที่เหมาะสมกว่าบล็อกรหัส โปรดหยุดการใส่ข้อความของคุณลงในบล็อคโค้ดเนื่องจากไม่ใช่โค้ดและไม่ได้รับประโยชน์จากแบบอักษรที่มีการเลื่อนแบบแยกส่วน
Peter Cordes

16
ฉันไม่เห็นจริงๆว่าวิธีนี้ตอบคำถาม นี่ไม่ใช่คำถามที่คลุมเครือเกี่ยวกับว่ารหัสแอสเซมบลีหรือรหัส C ++ อาจเร็วกว่านี้หรือไม่ - มันเป็นคำถามที่เฉพาะเจาะจงมากเกี่ยวกับรหัสจริงซึ่งเขาให้ความช่วยเหลืออย่างเป็นกันเองในคำถามนั้น คำตอบของคุณไม่ได้กล่าวถึงรหัสใด ๆ หรือทำการเปรียบเทียบประเภทใดก็ได้ แน่นอนว่าเคล็ดลับของคุณเกี่ยวกับวิธีการวัดมาตรฐานนั้นถูกต้อง แต่ไม่เพียงพอที่จะตอบคำถามจริง
Cody Gray

6

สำหรับปัญหา Collatz คุณสามารถเพิ่มประสิทธิภาพได้อย่างมากโดยการแคช "tails" นี่คือการแลกเปลี่ยนเวลา / หน่วยความจำ โปรดดู: การบันทึกช่วยจำ ( https://en.wikipedia.org/wiki/Memoization ) คุณสามารถค้นหาโซลูชันการเขียนโปรแกรมแบบไดนามิกสำหรับการแลกเปลี่ยนเวลา / หน่วยความจำอื่น ๆ

ตัวอย่างการนำ python มาใช้:

import sys

inner_loop = 0

def collatz_sequence(N, cache):
    global inner_loop

    l = [ ]
    stop = False
    n = N

    tails = [ ]

    while not stop:
        inner_loop += 1
        tmp = n
        l.append(n)
        if n <= 1:
            stop = True  
        elif n in cache:
            stop = True
        elif n % 2:
            n = 3*n + 1
        else:
            n = n // 2
        tails.append((tmp, len(l)))

    for key, offset in tails:
        if not key in cache:
            cache[key] = l[offset:]

    return l

def gen_sequence(l, cache):
    for elem in l:
        yield elem
        if elem in cache:
            yield from gen_sequence(cache[elem], cache)
            raise StopIteration

if __name__ == "__main__":
    le_cache = {}

    for n in range(1, 4711, 5):
        l = collatz_sequence(n, le_cache)
        print("{}: {}".format(n, len(list(gen_sequence(l, le_cache)))))

    print("inner_loop = {}".format(inner_loop))

1
คำตอบของ gnasher แสดงว่าคุณสามารถทำได้มากกว่าแค่แคชก้อย: บิตสูงไม่ส่งผลต่อสิ่งที่เกิดขึ้นต่อไปและเพิ่ม / mul เพียงกระจายไปทางซ้ายดังนั้นบิตสูงจึงไม่มีผลต่อสิ่งที่เกิดขึ้นกับบิตต่ำ นั่นคือคุณสามารถใช้การค้นหา LUT ไปที่ 8 (หรือจำนวนใด ๆ ) ของบิตในแต่ละครั้งด้วยการคูณและเพิ่มค่าคงที่เพื่อนำไปใช้กับส่วนที่เหลือของบิต การจดบันทึกส่วนท้ายนั้นมีประโยชน์ในหลาย ๆ ปัญหาเช่นนี้และสำหรับปัญหานี้เมื่อคุณยังไม่ได้คิดถึงวิธีที่ดีกว่าหรือยังไม่ได้พิสูจน์ว่าถูกต้อง
Peter Cordes

2
หากฉันเข้าใจความคิดของ gnasher ด้านบนอย่างถูกต้องฉันคิดว่าการบันทึกความจำส่วนหางเป็นการเพิ่มประสิทธิภาพแบบมุมฉาก ดังนั้นคุณน่าจะทำทั้งสองอย่าง มันจะน่าสนใจที่จะตรวจสอบว่าคุณจะได้รับเท่าไหร่จากการเพิ่มการบันทึกลงในอัลกอริทึมของ Gnasher
Emanuel Landeholm

2
เราอาจทำการบันทึกช่วยจำที่ถูกกว่าโดยเก็บเฉพาะส่วนที่หนาแน่นของผลลัพธ์ ตั้งค่าขีด จำกัด สูงสุดบน N และเหนือสิ่งนั้นอย่าตรวจสอบหน่วยความจำ ด้านล่างใช้ hash (N) -> N เป็นฟังก์ชัน hash ดังนั้น key = ตำแหน่งในอาร์เรย์และไม่จำเป็นต้องเก็บไว้ รายการ0วิธีการยังไม่ปรากฏ เราสามารถปรับให้เหมาะสมต่อไปโดยการจัดเก็บคี่ N ในตารางดังนั้นฟังก์ชันแฮชคือการn>>1ยกเลิก 1 เขียนโค้ดขั้นตอนเพื่อจบด้วยn>>tzcnt(n)หรือเพื่อให้แน่ใจว่ามันแปลก
Peter Cordes

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

2
คุณสามารถเก็บผลลัพธ์ที่คำนวณล่วงหน้าได้สำหรับ n <N ทั้งหมดสำหรับ N ที่มีขนาดใหญ่ดังนั้นคุณจึงไม่ต้องการค่าใช้จ่ายของตารางแฮช ข้อมูลในตารางนั้นจะถูกใช้ในที่สุดสำหรับทุกค่าเริ่มต้น หากคุณเพียงต้องการยืนยันว่าลำดับ Collatz สิ้นสุดลงใน (1, 4, 2, 1, 4, 2, ... ): นี่สามารถพิสูจน์ได้ว่าเทียบเท่ากับการพิสูจน์ว่าสำหรับ n> 1 ลำดับจะสิ้นสุดลงในที่สุด น้อยกว่าเดิม และสำหรับสิ่งนั้นหางแคชจะไม่ช่วย
gnasher729

5

จากความคิดเห็น:

แต่รหัสนี้ไม่เคยหยุด (เพราะจำนวนเต็มล้น)!?! Yves Daoust

สำหรับตัวเลขจำนวนมากมันจะไม่ล้น

ถ้ามันจะล้น - สำหรับเมล็ดแรกที่โชคร้ายเหล่านั้นจำนวนที่มากเกินไปจะมีแนวโน้มที่จะมารวมกันเป็น 1 โดยไม่มีการไหลล้นอีก

นี่เป็นคำถามที่น่าสนใจ แต่มีจำนวนเมล็ดมากเกินไปหรือไม่?

ชุดบรรจบขั้นสุดท้ายแบบเรียบง่ายเริ่มต้นด้วยพลังของสองค่า

2 ^ 64 จะล้นไปเป็นศูนย์ซึ่งไม่ได้กำหนดวงวนไม่สิ้นสุดตามอัลกอริทึม (ลงท้ายด้วย 1 เท่านั้น) แต่คำตอบที่ดีที่สุดในคำตอบจะเสร็จสิ้นเนื่องจากการshr raxผลิต ZF = 1

เราสามารถผลิต 2 ^ 64 ได้ไหม? หากจำนวนเริ่มต้นคือ0x5555555555555555มันเป็นเลขคี่จำนวนต่อไปคือแล้ว 3n + 1 ซึ่งเป็น=0xFFFFFFFFFFFFFFFF + 1 0ในทางทฤษฎีในสถานะที่ไม่ได้กำหนดของอัลกอริทึม แต่คำตอบที่ดีที่สุดของ johnfound จะกู้คืนโดยออกจาก ZF = 1 The cmp rax,1Peter Cordes จะสิ้นสุดในวงวนไม่สิ้นสุด (ตัวแปร QED 1, "cheapo" ผ่าน0หมายเลขที่ไม่ได้กำหนด)

จำนวนที่ซับซ้อนมากขึ้นซึ่งจะสร้างวงจรโดยไม่ต้อง0? ตรงไปตรงมาฉันไม่แน่ใจว่าทฤษฎีคณิตศาสตร์ของฉันมืดเกินไปที่จะได้รับความคิดที่จริงจังวิธีการจัดการกับมันอย่างจริงจัง แต่อย่างสังหรณ์ใจฉันจะบอกว่าซีรีส์นี้จะรวมกันเป็น 1 สำหรับทุกหมายเลข: 0 <number เนื่องจากสูตร 3n + 1 จะค่อยๆเปลี่ยนทุกปัจจัยที่ไม่ใช่ 2 ของจำนวนดั้งเดิม (หรือระดับกลาง) ให้กลายเป็นกำลัง 2 ไม่ช้าก็เร็ว . ดังนั้นเราไม่จำเป็นต้องกังวลเกี่ยวกับวงวนอนันต์สำหรับซีรี่ส์ดั้งเดิมมีเพียงล้นเท่านั้นที่สามารถขัดขวางเราได้

ดังนั้นฉันแค่ใส่ตัวเลขจำนวนน้อยลงในชีตแล้วดูตัวเลขที่ถูกตัดทอน 8 บิต

มีสามค่าล้นที่จะมี0: 227, 170และ85( 85ไปโดยตรงไปยัง0อีกสองพัฒนาไปสู่85)

แต่ไม่มีค่าที่จะสร้างเมล็ดล้นวงจร

สนุกพอฉันตรวจสอบซึ่งเป็นหมายเลขแรกที่ต้องทนทุกข์ทรมานจากการตัด 8 บิตและได้27รับผลกระทบแล้ว! มันเข้าถึงค่า9232ในซีรีส์ที่ไม่ถูกตัดทอนที่เหมาะสม (ค่าที่ถูกตัดทอนแรกอยู่322ในขั้นตอนที่ 12) และค่าสูงสุดที่มาถึงสำหรับหมายเลขอินพุต 2-255 ใด ๆ ในวิธีที่ไม่ถูกตัดทอนคือ13120(สำหรับ255ตัวเอง) จำนวนขั้นสูงสุด เพื่อรวมเข้าด้วยกัน1เป็นเรื่องเกี่ยวกับ128(+ -2 ไม่แน่ใจว่า "1" จะนับหรือไม่ ... )

น่าสนใจพอ (สำหรับฉัน) จำนวน9232สูงสุดสำหรับหมายเลขแหล่งข้อมูลอื่น ๆ มีอะไรพิเศษเกี่ยวกับมัน : -O 9232= 0x2410... hmmm .. ไม่มีความคิด

แต่น่าเสียดายที่ฉันไม่สามารถได้รับการเข้าใจลึกใด ๆ ของชุดนี้ทำไมมันมาบรรจบกันและสิ่งที่เป็นผลกระทบของการตัดทอนให้กับkบิต แต่มีcmp number,1เงื่อนไขการยกเลิกเป็นไปได้อย่างแน่นอนที่จะนำอัลกอริทึมลงในวง จำกัด ที่มีมูลค่าการป้อนข้อมูลโดยเฉพาะอย่างยิ่งสิ้นสุดวันที่0หลังจาก การตัด

แต่ค่าที่มาก27เกินไปสำหรับกรณี 8 บิตเป็นการเรียงลำดับของการแจ้งเตือนดูเหมือนว่าถ้าคุณนับจำนวนขั้นตอนที่จะเข้าถึงมูลค่า1คุณจะได้รับผลลัพธ์ที่ผิดสำหรับตัวเลขส่วนใหญ่จากชุดจำนวนเต็ม k-bit ทั้งหมด สำหรับเลขจำนวนเต็ม 8 บิต 146 หมายเลขจาก 256 ได้รับผลกระทบแบบอนุกรมโดยการตัด (บางคนอาจยังคงโดนจำนวนขั้นตอนที่ถูกต้องโดยไม่ตั้งใจอาจจะขี้เกียจตรวจสอบ)


"จำนวนที่มากเกินไปน่าจะมารวมกันเป็น 1 โดยไม่มีการล้นมากเกินไป": รหัสไม่เคยหยุดนิ่ง (นั่นคือการคาดเดาที่ผมไม่สามารถรอจนกว่าจะสิ้นสุดของครั้งเพื่อให้แน่ใจว่า ... )
Yves Daoust

@YvesDaoust โอ้ แต่มันทำ ... ตัวอย่างเช่น27ชุดที่มีการตัด 8b มีลักษณะเช่นนี้: 82 41 124 62 31 94 47 142 71 214 107 66 (ตัด) 33 100 50 25 76 38 19 58 29 88 44 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1 1 (ส่วนที่เหลือของมันทำงานได้โดยไม่ต้องตัดทอน) ฉันไม่ได้รับคุณขอโทษ มันจะไม่มีวันหยุดถ้าค่าที่ถูกตัดทอนนั้นมีค่าเท่ากับบางส่วนของค่าก่อนหน้านี้ในซีรีย์ที่กำลังดำเนินอยู่และฉันไม่สามารถหาค่าดังกล่าวเทียบกับการตัดปลาย k-bit (แต่ฉันก็ไม่สามารถหาทฤษฎีคณิตศาสตร์ได้ นี่ถือเป็นการตัดปลาย 8/16/32/64 บิตโดยสังหรณ์ใจฉันคิดว่ามันใช้งานได้)
Ped7g

1
ฉันควรตรวจสอบคำอธิบายปัญหาดั้งเดิมเร็วกว่า: "แม้ว่าจะยังไม่ได้รับการพิสูจน์ (ปัญหาโคลลาตซ์) ก็คิดว่าตัวเลขเริ่มต้นทั้งหมดจะเสร็จสิ้นที่ 1" ... ตกลงน่าแปลกใจที่ฉันไม่สามารถได้รับการเข้าใจของมันที่มีความรู้ทางคณิตศาสตร์ของฉัน จำกัด หมอกไม่ ... : D และจากการทดลองแผ่นของฉันฉันสามารถมั่นใจได้ว่ามันจะมาบรรจบกันสำหรับทุกคน2- 255จำนวนอย่างใดอย่างหนึ่งโดยไม่ต้องตัด (เพื่อ1) หรือด้วยการตัด 8 บิต (ทั้งที่คาดไว้1หรือ0ตัวเลขสามตัว)
Ped7g

เฮ็มเมื่อฉันบอกว่าไม่หยุดฉันหมายความว่า ... มันไม่หยุด รหัสที่กำหนดจะทำงานตลอดไปหากคุณต้องการ
Yves Daoust

1
โหวตขึ้นสำหรับการวิเคราะห์สิ่งที่เกิดขึ้นกับโอเวอร์โฟลว์ การวนรอบแบบ CMP สามารถใช้cmp rax,1 / jna(เช่นdo{}while(n>1)) เพื่อยุติด้วยเช่นกัน ฉันคิดเกี่ยวกับการสร้างลูปเวอร์ชันที่บรรเลงซึ่งบันทึกค่าสูงสุดที่nเห็นเพื่อให้ทราบว่าเราเข้าใกล้ล้นได้อย่างไร
Peter Cordes

5

คุณไม่ได้โพสต์โค้ดที่คอมไพเลอร์สร้างดังนั้นจึงมีการคาดเดาบางอย่างที่นี่ แต่ถึงแม้จะไม่ได้เห็นมันก็สามารถพูดได้ว่า:

test rax, 1
jpe even

... มีโอกาส 50% ในการคาดเดาผิดสาขาและนั่นจะมีราคาแพง

คอมไพเลอร์ทำทั้งสองอย่างแน่นอนการคำนวณ (ซึ่งเสียค่าใช้จ่ายมากขึ้นตั้งแต่ div / mod ค่อนข้างล่าช้าดังนั้นการคูณเพิ่มคือ "ฟรี") และติดตามด้วย CMOV ซึ่งแน่นอนว่ามีโอกาสร้อยละศูนย์ในการถูกตัดสินผิด


1
มีรูปแบบบางอย่างที่จะแตกแขนง; เช่นจำนวนคี่จะตามด้วยหมายเลขคู่เสมอ แต่บางครั้ง 3n + 1 จะทิ้งบิตศูนย์ต่อท้ายหลาย ๆ อันและนั่นคือเมื่อมันจะผิด ฉันเริ่มเขียนเกี่ยวกับการแบ่งในคำตอบของฉันและไม่ได้ระบุธงสีแดงขนาดใหญ่อื่น ๆ นี้ในรหัสของ OP (โปรดทราบว่าการใช้เงื่อนไขแบบพาริตี้นั้นแปลกมากเมื่อเทียบกับ JZ หรือ CMOVZ มันแย่กว่าสำหรับซีพียูเพราะซีพียูของ Intel สามารถแมคโคร TEST / JZ ได้ แต่ไม่ใช่ TEST / JPE Agner Fog กล่าวว่า AMD สามารถหลอมรวมได้ ทดสอบ / CMP กับ JCC ใด ๆ ดังนั้นในกรณีนี้จะเลวร้ายยิ่งขึ้นสำหรับผู้อ่านที่เป็นมนุษย์)
Peter Cordes

5

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

แก้ไข: ระยะทางของคุณอาจแตกต่างกันในคำสั่ง "การหารจำนวนเต็มเร็วกว่าการหารจุดลอยตัว" ด้านบน ความคิดเห็นด้านล่างแสดงให้เห็นว่าหน่วยประมวลผลที่ทันสมัยได้จัดลำดับความสำคัญการเพิ่มประสิทธิภาพการแบ่ง fp มากกว่าการแบ่งจำนวนเต็ม ดังนั้นถ้าใครกำลังหาเหตุผลได้มากที่สุดสำหรับการเร่งความเร็วที่คำถามของหัวข้อนี้ถามเกี่ยวกับการเพิ่มประสิทธิภาพแล้วคอมไพเลอร์/=2เป็น>>=1จะเป็นสถานที่ที่ดีที่สุดในวันที่ 1 ดู


ในบันทึกที่ไม่เกี่ยวข้องถ้าnเป็นเลขคี่นิพจน์n*3+1จะเป็นเลขคู่เสมอ ดังนั้นไม่จำเป็นต้องตรวจสอบ คุณสามารถเปลี่ยนสาขานั้นเป็น

{
   n = (n*3+1) >> 1;
   count += 2;
}

ดังนั้นข้อความทั้งหมดจะเป็น

if (n & 1)
{
    n = (n*3 + 1) >> 1;
    count += 2;
}
else
{
    n >>= 1;
    ++count;
}

4
การแบ่งจำนวนเต็มไม่ได้เร็วกว่าการแบ่ง FP บนซีพียู x86 ที่ทันสมัย ฉันคิดว่านี่เป็นเพราะ Intel / AMD ใช้จ่ายทรานซิสเตอร์มากขึ้นในตัวแบ่ง FP ของพวกเขาเพราะมันเป็นการดำเนินการที่สำคัญกว่า (การหารจำนวนเต็มด้วยค่าคงที่สามารถปรับให้เหมาะสมกับการคูณด้วยอินเวอร์สแบบแยกส่วน) ตรวจสอบตาราง insn ของ Agner Fog และเปรียบเทียบ DIVSD (ทศนิยมสองเท่า) กับDIV r32(จำนวนเต็ม 32 บิตที่ไม่ได้ลงชื่อ) หรือDIV r64( จำนวนเต็ม64 บิตที่ไม่ได้ลงชื่อช้ากว่า) โดยเฉพาะอย่างยิ่งสำหรับปริมาณงานการแบ่ง FP นั้นเร็วกว่ามาก (uop เดี่ยวแทนที่จะใช้ไมโครโค้ดและ pipelined บางส่วน) แต่เวลาในการตอบสนองก็ดีขึ้นเช่นกัน
Peter Cordes

1
เช่นบน Haswell CPU: DIVSD คือ 1 uop, 10-20 รอบแฝง, หนึ่งต่อ 8-14c throughput div r64คือ 36 uops, 32-96c latency และหนึ่งต่อ 21-74c throughput Skylake มีปริมาณงานหาร FP ที่เร็วขึ้น (ส่งต่อที่หนึ่งต่อ 4c โดยมีความหน่วงแฝงที่ไม่ดีกว่า) แต่ก็ไม่ได้เร็วกว่าจำนวนเต็ม div สิ่งที่มีความคล้ายคลึงกับ AMD Bulldozer-family: DIVSD คือ 1M-op, 9-27c latency, หนึ่งต่อ 4.5-11c throughput div r64คือ 16M-ops, 16-75c latency, หนึ่งต่อ 16-75c throughput
Peter Cordes

1
การหาร FP นั้นไม่ใช่แบบเดียวกับเลขชี้กำลังจำนวนเต็มลบเลขแทนแมนทิสสาของจำนวนเต็มหารหารตรวจจับ denormals หรือไม่ และขั้นตอนทั้งสามนั้นสามารถทำคู่ขนานกันได้
MSalters

2
@MSalters: ใช่เสียงที่ถูกต้อง แต่มีขั้นตอนการฟื้นฟูที่ปลาย ot บิตบิตระหว่างเลขชี้กำลังและ mantiss doubleมี Mantissa 53 บิต แต่ก็ยังช้ากว่าdiv r32Haswell อย่างมาก ดังนั้นมันจึงเป็นเรื่องของฮาร์ดแวร์ที่ Intel / AMD ขว้างปัญหาไปเพราะพวกเขาไม่ได้ใช้ทรานซิสเตอร์ตัวเดียวกันสำหรับทั้งจำนวนเต็มและตัวหาร fp จำนวนเต็มหนึ่งเป็นเซนต์คิตส์และเนวิส (ไม่มีหารจำนวนเต็ม-SIMD) และเวกเตอร์หนึ่งจับ 128b เวกเตอร์ (ไม่ 256b เหมือนเวกเตอร์ ALU อื่น ๆ ) สิ่งใหญ่คือ div จำนวนเต็มเป็น uops มากมายส่งผลกระทบต่อรหัสโดยรอบ
Peter Cordes

เอ่อไม่ใช่กะบิตระหว่างแมนทิสซากับเลขชี้กำลัง แต่เปลี่ยนแมนทิสซาให้เป็นปกติด้วยการกะและเพิ่มจำนวนกะให้แก่เลขชี้กำลัง
Peter Cordes

4

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

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

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


2
เห็นด้วยกับสิ่งนี้โดยสิ้นเชิง gcc -O3ทำรหัสที่อยู่ในระยะ 20% ของแฮ็คที่ดีที่สุดสำหรับอัลกอริทึมที่แน่นอน (การได้รับการเร่งความเร็วเหล่านั้นเป็นจุดสนใจหลักของคำตอบของฉันเท่านั้นเพราะนั่นคือคำถามที่ถามและมีคำตอบที่น่าสนใจไม่ใช่เพราะมันเป็นวิธีการที่ถูกต้อง) การเร่งความเร็วที่ใหญ่กว่านั้นได้มาจากการเปลี่ยนแปลงที่คอมไพเลอร์ เช่นการเลื่อนการเลื่อนขวาหรือการทำทีละ 2 ขั้นตอน การเพิ่มความเร็วที่ใหญ่กว่าที่ทำได้จากการบันทึก / การค้นหาตาราง ยังคงการทดสอบอย่างละเอียด แต่ไม่ใช่แรงเดรัจฉานบริสุทธิ์
Peter Cordes

2
ยังมีการใช้งานง่ายที่เห็นได้ชัดว่าถูกต้องมีประโยชน์อย่างยิ่งสำหรับการทดสอบการใช้งานอื่น ๆ สิ่งที่ฉันจะทำก็แค่ดูที่เอาต์พุต asm เพื่อดูว่า gcc ทำมันไม่ไร้สาระเหมือนที่ฉันคาดไว้หรือไม่
Peter Cordes

-2

คำตอบง่ายๆ:

  • การทำ MOV RBX, 3 และ MUL RBX นั้นมีราคาแพง เพียงแค่เพิ่ม RBX, RBX สองครั้ง

  • เพิ่ม 1 อาจเร็วกว่า INC ที่นี่

  • MOV 2 และ DIV มีราคาแพงมาก แค่เลื่อนไปทางขวา

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

ถ้าคุณสร้างรายการแอสเซมบลีสำหรับโปรแกรม C ++ ของคุณคุณสามารถดูความแตกต่างจากแอสเซมบลีของคุณ


4
1): การเพิ่ม 3 ครั้งจะเป็นใบ้เมื่อเทียบกับหน่วยงาน LEA นอกจากนี้mul rbxใน Haswell CPU ของ OP ยังเป็น 2 uops ที่มีความหน่วง 3c (และ 1 ต่อทรูพุตของนาฬิกา) imul rcx, rbx, 3มีเพียง 1 uop โดยมีเวลาหน่วง 3c เท่ากัน คำแนะนำการเพิ่มสองคำสั่งจะเป็น 2 uops ที่มีเวลาแฝง 2c
Peter Cordes

5
2) เพิ่ม 1 อาจจะเร็วกว่า INC อยู่ที่นี่ ไม่ OP ไม่ได้ใช้ Pentium 4 จุดที่คุณ 3) เป็นเพียงส่วนที่ถูกต้องของคำตอบนี้
Peter Cordes

5
4) ดูเหมือนเรื่องไร้สาระทั้งหมด รหัส 64 บิตอาจช้าลงด้วยโครงสร้างข้อมูลตัวชี้หนักเนื่องจากพอยน์เตอร์ขนาดใหญ่หมายถึงการปล่อยแคชที่ใหญ่กว่า แต่รหัสนี้ใช้งานได้เฉพาะในการลงทะเบียนและปัญหาการจัดตำแหน่งรหัสจะเหมือนกันในโหมด 32 และ 64 บิต (เช่นปัญหาการจัดตำแหน่งข้อมูลไม่มีเงื่อนงำสิ่งที่คุณกำลังพูดถึงด้วยการจัดตำแหน่งเป็นปัญหาใหญ่สำหรับ x86-64) อย่างไรก็ตามรหัสไม่ได้สัมผัสหน่วยความจำภายในลูป
Peter Cordes

ผู้แสดงความคิดเห็นไม่รู้ว่ากำลังพูดถึงอะไร ทำ MOV + MUL บน CPU 64 บิตจะช้ากว่าสามเท่าเมื่อเพิ่มการลงทะเบียนในตัวเองสองครั้ง ข้อสังเกตอื่น ๆ ของเขาไม่ถูกต้องเท่าเทียมกัน
Tyler Durden

6
MOV + MUL นั้นโง่ แต่ MOV + เพิ่ม + ยังคงโง่อยู่ (จริง ๆ แล้วการADD RBX, RBXคูณสองคูณด้วย 4 ไม่ใช่ 3) lea rax, [rbx + rbx*2]ไกลโดยวิธีที่ดีที่สุดคือ หรือด้วยค่าใช้จ่ายในการทำให้เป็น 3 องค์ประกอบ LEA ให้ทำ +1 ด้วยlea rax, [rbx + rbx*2 + 1] (3c latency บน HSW แทนที่จะเป็น 1 ตามที่อธิบายไว้ในคำตอบ) ประเด็นของฉันคือ 64- บิตนั้นไม่แพงมาก Intel CPUs ล่าสุดเนื่องจากมีหน่วยคูณจำนวนเต็มอย่างรวดเร็วอย่างไม่น่าเชื่อ (แม้เมื่อเปรียบเทียบกับ AMD ซึ่งMUL r64มีความหน่วงแฝง 6c ด้วยอัตราการส่งข้อมูลหนึ่งต่อ 4c: ไม่มีการวางท่ออย่างเต็มที่
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.