ทำไม GCC ไม่สามารถสันนิษฐานได้ว่า std :: vector :: size จะไม่เปลี่ยนแปลงในลูปนี้


14

ฉันอ้างว่าเป็นเพื่อนร่วมงานที่if (i < input.size() - 1) print(0);จะได้รับการเพิ่มประสิทธิภาพในวงนี้เพื่อที่input.size()จะไม่ได้อ่านทุกครั้ง แต่มันกลับกลายเป็นว่านี่ไม่ใช่กรณี!

void print(int x) {
    std::cout << x << std::endl;
}

void print_list(const std::vector<int>& input) {
    int i = 0;
    for (size_t i = 0; i < input.size(); i++) {
        print(input[i]);
        if (i < input.size() - 1) print(0);
    }
}

ตามCompiler Explorerพร้อมตัวเลือก gcc -O3 -fno-exceptionsเรากำลังอ่านการinput.size()วนซ้ำแต่ละครั้งและใช้leaเพื่อทำการลบ!

        movq    0(%rbp), %rdx
        movq    8(%rbp), %rax
        subq    %rdx, %rax
        sarq    $2, %rax
        leaq    -1(%rax), %rcx
        cmpq    %rbx, %rcx
        ja      .L35
        addq    $1, %rbx

ที่น่าสนใจใน Rust การเพิ่มประสิทธิภาพนี้เกิดขึ้น ดูเหมือนว่าiจะได้รับการแทนที่ด้วยตัวแปรjที่เป็น decremented ซ้ำกันและการทดสอบจะถูกแทนที่ด้วยสิ่งที่ชอบi < input.size() - 1j > 0

fn print(x: i32) {
    println!("{}", x);
}

pub fn print_list(xs: &Vec<i32>) {
    for (i, x) in xs.iter().enumerate() {
        print(*x);
        if i < xs.len() - 1 {
            print(0);
        }
    }
}

ในคอมไพเลอร์ Explorerแอสเซมบลีที่เกี่ยวข้องมีลักษณะดังนี้:

        cmpq    %r12, %rbx
        jae     .LBB0_4

ฉันตรวจสอบและฉันค่อนข้างแน่ใจว่าr12เป็นxs.len() - 1และrbxเป็นเคาน์เตอร์ ก่อนหน้านี้มีการaddสำหรับrbxและด้านนอกของวงเข้าmovr12

ทำไมนี้ ดูเหมือนว่าหาก GCC สามารถอินไลน์ได้size()และoperator[]อย่างที่มันทำมันควรจะรู้ว่าsize()มันไม่เปลี่ยนแปลง แต่เครื่องมือเพิ่มประสิทธิภาพของ GCC อาจตัดสินว่ามันไม่คุ้มค่าที่จะดึงมันออกมาเป็นตัวแปรหรือไม่? หรืออาจมีผลข้างเคียงอื่น ๆ ที่อาจทำให้สิ่งนี้ไม่ปลอดภัย - ไม่มีใครรู้หรือไม่?


1
นอกจากนี้ยังprintlnอาจจะเป็นวิธีการที่ซับซ้อนคอมไพเลอร์อาจจะมีปัญหาในการพิสูจน์ว่าprintlnไม่กลายพันธุ์เวกเตอร์
Mooing Duck

1
@MooingDuck: อีกเธรดหนึ่งจะเป็น data-race UB คอมไพเลอร์สามารถและคิดว่าจะไม่เกิดขึ้น cout.operator<<()นี่คือปัญหาการเรียกฟังก์ชั่นที่ไม่ใช่แบบอินไลน์ไป คอมไพเลอร์ไม่ทราบว่าฟังก์ชั่นกล่องดำนี้ไม่ได้รับการอ้างอิงถึงstd::vectorจากโกลบอล
Peter Cordes

@PeterCordes: คุณพูดถูกว่ากระทู้อื่น ๆ ไม่ใช่คำอธิบายแบบสแตนด์อโลนและความซับซ้อนของprintlnหรือoperator<<เป็นกุญแจ
Mooing Duck

คอมไพเลอร์ไม่ทราบความหมายของวิธีการภายนอกเหล่านี้
user207421

คำตอบ:


10

การเรียกใช้ฟังก์ชั่นที่ไม่ใช่แบบอินไลน์cout.operator<<(int)เป็นกล่องดำสำหรับเครื่องมือเพิ่มประสิทธิภาพ (เพราะห้องสมุดเพิ่งเขียนใน C ++ และเครื่องมือเพิ่มประสิทธิภาพทั้งหมดเห็นเป็นแบบตัวอย่างดูการอภิปรายในความคิดเห็น) จะต้องถือว่าหน่วยความจำใด ๆ ที่อาจจะชี้ไปโดยส่วนกลาง var ได้รับการแก้ไข

(หรือการstd::endlเรียกใช้ BTW เหตุใดจึงต้องล้างข้อมูลที่จุดนั้นแทนที่จะพิมพ์เพียง'\n'?)

เช่นสำหรับทุกมันรู้std::vector<int> &inputคือการอ้างอิงถึงตัวแปรทั่วโลกและเป็นหนึ่งในฟังก์ชั่นเหล่านั้นเรียกว่าปรับเปลี่ยน var (หรือมีทั่วโลกvector<int> *ptrที่ไหนสักแห่งหรือมีฟังก์ชั่นที่ส่งกลับตัวชี้ไปยังstatic vector<int>หน่วยรวบรวมอื่น ๆ หรือวิธีอื่นที่ฟังก์ชันสามารถรับการอ้างอิงเวกเตอร์นี้ได้โดยไม่ต้องผ่านการอ้างอิงจากเรา

หากคุณมีตัวแปรโลคัลที่ไม่เคยมีที่อยู่คอมไพเลอร์อาจสันนิษฐานได้ว่าการเรียกฟังก์ชั่นที่ไม่ใช่อินไลน์ไม่สามารถกลายพันธุ์ได้ เพราะไม่มีทางที่ตัวแปรโกลบอลจะถือตัวชี้ไปยังวัตถุนี้ ( นี่เรียกว่าการวิเคราะห์การหลบหนี ) นั่นเป็นสาเหตุที่คอมไพเลอร์สามารถเก็บsize_t iในการลงทะเบียนระหว่างการเรียกใช้ฟังก์ชัน ( int iสามารถปรับให้เหมาะกับการใช้งานเพราะมันเป็นเงาsize_t iและไม่ได้ใช้อย่างอื่น)

มันสามารถทำเช่นเดียวกันกับท้องถิ่นvector(เช่นสำหรับตัวชี้ฐาน end_size และ end_capacity)

ISO C99 int *restrict fooมีทางออกสำหรับปัญหานี้: หลาย c ++ คอมไพล์สนับสนุนint *__restrict fooให้สัญญาหน่วยความจำที่ชี้ไปตามfooเป็นเพียงเข้าถึงได้ผ่านทางตัวชี้ว่า มีประโยชน์มากที่สุดในฟังก์ชั่นที่ใช้ 2 อาร์เรย์และคุณต้องการสัญญากับคอมไพเลอร์ที่ไม่ทับซ้อนกัน ดังนั้นมันสามารถทำให้เวกเตอร์อัตโนมัติโดยไม่ต้องสร้างรหัสเพื่อตรวจสอบและเรียกใช้วนรอบทางเลือก

ความคิดเห็น OP:

ในสนิมการอ้างอิงที่ไม่สามารถเปลี่ยนแปลงได้คือการรับประกันทั่วโลกว่าไม่มีใครเปลี่ยนแปลงค่าที่คุณมีการอ้างอิงถึง (เทียบเท่ากับ C ++ restrict)

นั่นอธิบายว่าเหตุใด Rust จึงสามารถเพิ่มประสิทธิภาพนี้ได้ แต่ C ++ ไม่สามารถทำได้


เพิ่มประสิทธิภาพ C ++ ของคุณ

เห็นได้ชัดว่าคุณควรใช้auto size = input.size();หนึ่งครั้งที่ด้านบนสุดของฟังก์ชั่นเพื่อให้คอมไพเลอร์รู้ว่ามันเป็นค่าคงที่แบบวนซ้ำ การใช้ C ++ ไม่สามารถแก้ปัญหานี้ให้คุณได้ดังนั้นคุณต้องทำด้วยตัวเอง

คุณอาจต้องconst int *data = input.data();ยกตัวชี้ข้อมูลจากstd::vector<int>"บล็อกควบคุม" เช่นกัน เป็นเรื่องโชคร้ายที่การเพิ่มประสิทธิภาพอาจต้องมีการเปลี่ยนแปลงแหล่งที่มา

Rust เป็นภาษาที่ทันสมัยมากขึ้นได้รับการออกแบบหลังจากนักพัฒนาคอมไพเลอร์ได้เรียนรู้สิ่งที่เป็นไปได้ในทางปฏิบัติสำหรับคอมไพเลอร์ มันแสดงให้เห็นจริงๆในรูปแบบอื่น ๆ ด้วยรวมทั้ง portably เผยให้เห็นบางส่วนของเย็นซีพียูสิ่งที่สามารถทำได้ผ่านทางi32.count_ones, หมุน, บิตสแกน ฯลฯ มันโง่จริงๆที่ ISO C ++ ยังคงไม่เปิดเผยใด ๆ เหล่านี้ portably std::bitset::count()ยกเว้น


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

1
มาตรฐานกำหนดพฤติกรรมของoperator<<ประเภทตัวถูกดำเนินการเหล่านั้น ดังนั้นในมาตรฐาน C ++ ไม่ใช่กล่องดำและคอมไพเลอร์สามารถสันนิษฐานได้ว่ามันทำตามที่เอกสารระบุไว้ บางทีพวกเขาต้องการสนับสนุนนักพัฒนาห้องสมุดที่เพิ่มพฤติกรรมที่ไม่เป็นมาตรฐาน ...
MM

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

2
@MM มันไม่ได้พูดถึงวัตถุที่สุ่มฉันพูดว่าเวกเตอร์ที่ใช้งานที่กำหนดไว้ ไม่มีอะไรในมาตรฐานที่ห้ามการใช้งานจากการมีเวกเตอร์ที่ใช้งานที่กำหนดไว้ซึ่งตัวดำเนินการ << แก้ไขและอนุญาตให้เข้าถึงเวกเตอร์นี้ในลักษณะที่กำหนดไว้ในการนำไปใช้ coutช่วยให้เป้าหมายของผู้ใช้ระดับที่กำหนดมาจากจะเกี่ยวข้องกับการสตรีมโดยใช้streambuf cout.rdbufในทำนองเดียวกันวัตถุที่ได้มาจากสามารถเชื่อมโยงกับostream cout.tie
Ross Ridge

2
@PeterCordes - ฉันจะไม่มั่นใจเกี่ยวกับเวกเตอร์ท้องถิ่น: ทันทีที่ฟังก์ชั่นสมาชิกใด ๆ หลุดออกจากแถวชาวบ้านก็หนีไปได้อย่างมีประสิทธิภาพเพราะthisตัวชี้ถูกส่งผ่านโดยปริยาย เรื่องนี้อาจเกิดขึ้นในทางปฏิบัติเร็วที่สุดเท่าที่ผู้สร้าง ลองพิจารณาลูปแบบง่ายนี้ - ฉันตรวจสอบลูปหลักของ gcc เท่านั้น (จากL34:ถึงjne L34) แต่มันทำงานได้อย่างแน่นอนราวกับว่าสมาชิกเวกเตอร์ได้หลบหนี (โหลดจากหน่วยความจำซ้ำแต่ละครั้ง)
BeeOnRope
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.