ฉันสามารถใบ้ตัวเพิ่มประสิทธิภาพโดยให้ช่วงของจำนวนเต็มได้หรือไม่


173

ฉันใช้intประเภทเพื่อเก็บค่า โดยความหมายของโปรแกรมค่าแตกต่างกันเสมอในช่วงที่เล็กมาก (0 - 36) และint(ไม่ใช่ a char) ถูกใช้เพียงเพราะประสิทธิภาพของ CPU

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

ดังนั้นเป็นไปได้หรือไม่ที่จะบอกคอมไพเลอร์ว่านี่intเป็นช่วงเล็ก ๆ เสมอและเป็นไปได้หรือไม่ที่คอมไพเลอร์จะทำการปรับแต่งเหล่านั้นให้ดีที่สุด?


4
การปรับช่วงค่าให้มีอยู่ในคอมไพเลอร์จำนวนมากเช่น llvmแต่ฉันไม่ได้ตระหนักถึงคำใบ้ภาษาใด ๆ ที่จะประกาศ
Remus Rusanu

2
โปรดทราบว่าหากคุณไม่เคยมีตัวเลขติดลบคุณอาจได้รับประโยชน์เล็กน้อยจากการใช้unsignedประเภทเนื่องจากจะง่ายต่อการคอมไพเลอร์เพื่อให้เหตุผล
user694733

4
@RemusRusanu: ปาสกาลช่วยให้คุณกำหนดประเภท subrangevar value: 0..36;เช่น
Edgar Bonet

7
" int (ไม่ใช่ตัวอักษร) ใช้เพียงเพราะประสิทธิภาพของ CPU " ชิ้นส่วนเก่า ๆ ของภูมิปัญญาดั้งเดิมนี้มักจะไม่เป็นความจริง บางครั้งประเภทที่แคบจะต้องเป็นศูนย์หรือขยายสัญญาณไปยังความกว้างของการลงทะเบียนแบบเต็มโดยเฉพาะ เมื่อใช้เป็นดัชนีอาเรย์ แต่บางครั้งก็เกิดขึ้นฟรี หากคุณมีประเภทนี้มากมายการลดขนาดของแคชมักจะมีค่ามากกว่าอย่างอื่น
Peter Cordes

1
ลืมที่จะพูดว่า: intและunsigned intต้องมีการลงชื่อ - หรือขยายเป็นศูนย์จาก 32 ถึง 64 บิตเช่นกันในระบบส่วนใหญ่ที่มีตัวชี้ 64- บิต โปรดทราบว่าใน x86-64 การดำเนินการกับการลงทะเบียนแบบ 32- บิตเป็นศูนย์ขยายเป็น 64- บิตฟรี (ไม่ใช่การขยายสัญญาณ แต่การโอเวอร์โฟลว์ที่ลงชื่อแล้วนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดดังนั้นคอมไพเลอร์สามารถใช้คณิตศาสตร์ ดังนั้นคุณจะเห็นคำแนะนำพิเศษสำหรับฟังก์ชั่น zero-expand 32-bit args ไม่ใช่ผลลัพธ์ของการคำนวณ คุณต้องการประเภทที่ไม่ได้ลงนามแคบกว่า
Peter Cordes

คำตอบ:


230

ใช่มันเป็นไปได้ ตัวอย่างเช่นgccคุณสามารถใช้__builtin_unreachableบอกคอมไพเลอร์เกี่ยวกับเงื่อนไขที่เป็นไปไม่ได้เช่น:

if (value < 0 || value > 36) __builtin_unreachable();

เราสามารถห่อเงื่อนไขด้านบนในแมโคร:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

และใช้มันอย่างนั้น:

assume(x >= 0 && x <= 10);

ที่คุณสามารถดู , gccดำเนินการเพิ่มประสิทธิภาพบนพื้นฐานของข้อมูลนี้:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

ผลิต:

func(int):
    mov     eax, 17
    ret

ข้อเสียอย่างหนึ่งอย่างไรก็ตามหากรหัสของคุณเคยฝ่าฝืนสมมติฐานดังกล่าวคุณจะได้รับพฤติกรรมที่ไม่ได้กำหนดหากรหัสของคุณเคยแบ่งสมมติฐานดังกล่าวคุณจะได้รับพฤติกรรมไม่ได้กำหนด

จะไม่แจ้งให้คุณทราบเมื่อเกิดเหตุการณ์นี้แม้ในการตรวจแก้จุดบกพร่องสร้าง ในการดีบัก / ทดสอบ / จับข้อบกพร่องด้วยสมมติฐานได้ง่ายขึ้นคุณสามารถใช้ไฮบริดมาโครสมมติ / ยืนยัน (เครดิตถึง @David Z) เช่นนี้:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

ใน debug builds (ที่NDEBUG ไม่ได้กำหนดไว้) จะทำงานเหมือนassertข้อความแสดงข้อผิดพลาดทั่วไปและabortโปรแกรม 'ing' และใน build build นั้นจะใช้ประโยชน์จากการสันนิษฐานสร้างรหัสที่ดีที่สุด

อย่างไรก็ตามโปรดทราบว่ามันไม่ได้ใช้แทนงานปกติassert- condยังคงอยู่ในงานสร้างรีลีสดังนั้นคุณไม่ควรทำอะไรเช่นassume(VeryExpensiveComputation())นี้


5
@ Xofo ไม่ได้รับมันในตัวอย่างของฉันนี้เกิดขึ้นแล้วในขณะที่return 2สาขากำจัดจากรหัสโดยคอมไพเลอร์

6
อย่างไรก็ตามดูเหมือนว่า gcc จะไม่สามารถปรับการทำงานให้เหมาะสมกับการใช้เวทย์มนตร์หรือค้นหาตารางอย่างที่ OP ต้องการ
jingyu9575

19
@ user3528438 __builtin_expectเป็นคำใบ้ที่ไม่เข้มงวด __builtin_expect(e, c)ควรอ่านว่า " eน่าจะประเมินเป็นc" และอาจเป็นประโยชน์ในการเพิ่มประสิทธิภาพการทำนายสาขา แต่ไม่ได้ จำกัดeอยู่เสมอcดังนั้นไม่อนุญาตให้เครื่องมือเพิ่มประสิทธิภาพโยนกรณีอื่น ๆ ดูว่าสาขาที่มีการจัดในการชุมนุม

6
ในทางทฤษฎีรหัสใด ๆ __builtin_unreachable()ที่ไม่มีเงื่อนไขที่ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดไว้สามารถนำมาใช้ในสถานที่ของ
CodesInChaos

14
ถ้าไม่มีสิ่งแปลกประหลาดที่ฉันไม่ทราบเกี่ยวกับสิ่งนี้ทำให้เป็นความคิดที่ไม่ดีมันอาจเข้าท่าที่จะรวมสิ่งนี้เข้าด้วยassertกันเช่นกำหนดassumeว่าassertเมื่อNDEBUGไม่ได้กำหนดไว้และ__builtin_unreachable()เมื่อNDEBUGถูกกำหนด ด้วยวิธีนี้คุณจะได้รับประโยชน์จากข้อสมมติในรหัสการผลิต แต่ในการสร้างการดีบักคุณยังคงมีการตรวจสอบที่ชัดเจน แน่นอนว่าคุณต้องทำแบบทดสอบมากพอที่จะรับรองตัวเองว่าการสันนิษฐานจะเป็นที่พอใจในป่า
David Z

61

มีการสนับสนุนมาตรฐานสำหรับสิ่งนี้ สิ่งที่คุณควรทำคือการรวมstdint.h( cstdint) uint_fast8_tแล้วใช้ชนิด

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


2
ประเภทเหล่านี้ไม่ได้ใช้งานเกือบเท่าที่ควร (ฉันมักจะลืมว่ามีอยู่จริง) พวกเขาให้รหัสที่รวดเร็วและพกพาได้สวยมาก และพวกเขาได้รับรอบตั้งแต่ปี 1999
Lundin

นี่เป็นคำแนะนำที่ดีสำหรับกรณีทั่วไป คำตอบของ deniss แสดงวิธีแก้ปัญหาที่อ่อนโยนกว่าสำหรับสถานการณ์เฉพาะ
การแข่งขัน Lightness ใน Orbit

1
คอมไพเลอร์จะได้รับข้อมูลช่วง 0-255 บนระบบที่uint_fast8_tเป็นประเภท 8 บิต (เช่นunsigned char) เหมือนอยู่ใน x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ) บนต้นธันวาคมอัลฟาก่อน 21164Aโหลดไบต์ / ร้านค้าที่ไม่ได้รับการสนับสนุนเพื่อให้การดำเนินงานมีเหตุผลใด ๆ typedef uint32_t uint_fast8_tที่จะใช้ AFAIK ไม่มีกลไกสำหรับประเภทที่จะมีการ จำกัด ช่วงพิเศษกับคอมไพเลอร์ส่วนใหญ่ (เช่น gcc) ดังนั้นฉันค่อนข้างแน่ใจว่าuint_fast8_tจะทำงานเหมือนกับunsigned intหรืออะไรก็ตามในกรณีนี้
Peter Cordes

( boolเป็นพิเศษและ จำกัด ช่วงไว้ที่ 0 หรือ 1 แต่เป็นชนิดในตัวซึ่งไม่ได้กำหนดโดยไฟล์ส่วนหัวในแง่ของcharใน gcc / clang อย่างที่ฉันพูดฉันไม่คิดว่าคอมไพเลอร์ส่วนใหญ่มีกลไก ที่จะทำให้เป็นไปได้.)
Peter Cordes

1
อย่างไรก็ตามuint_fast8_tเป็นคำแนะนำที่ดีเพราะมันจะใช้ชนิด 8 unsigned intบิตบนแพลตฟอร์มที่ที่เป็นที่มีประสิทธิภาพ (จริง ๆ แล้วผมไม่แน่ใจว่าfastประเภทที่ควรจะเป็นได้อย่างรวดเร็วสำหรับการและไม่ว่าจะเป็นรอยเท้าแคชถ่วงดุลอำนาจควรจะเป็นส่วนหนึ่งของมัน.) x86 มีการสนับสนุนอย่างกว้างขวางสำหรับการดำเนินงานไบต์แม้กระทั่งการเพิ่มไบต์ด้วยแหล่งหน่วยความจำดังนั้นคุณไม่จำเป็นต้องทำการโหลดที่ขยายเป็นศูนย์แยกต่างหาก (ซึ่งก็ยังถูกมาก) gcc สร้างuint_fast16_tชนิด 64- บิตบน x86 ซึ่งไม่ได้ผลสำหรับการใช้งานส่วนใหญ่ (เทียบกับ 32- บิต) godbolt.org/g/Rmq5bv
Peter Cordes

8

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

สำหรับกรณีนี้ฉันพบว่าเทคนิคนี้สามารถทำงานได้:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

ความคิดที่เป็น tradeoff รหัสข้อมูล: คุณกำลังจะย้าย 1 บิตของข้อมูล (ไม่ว่าx == c) ลงในตรรกะการควบคุม
คำแนะนำนี้จะนำไปสู่เครื่องมือเพิ่มประสิทธิภาพที่xจริงแล้วเป็นค่าคงที่ที่รู้จักcสนับสนุนให้อินไลน์และเพิ่มประสิทธิภาพการเรียกครั้งแรกfooแยกจากส่วนที่เหลืออาจค่อนข้างหนัก

ตรวจสอบให้แน่ใจว่าได้แยกรหัสออกเป็นรูทีนย่อยเดียว foo - อย่าทำซ้ำโค้ด

ตัวอย่าง:

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

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

เพียงแค่ใช้-O3และแจ้งให้ทราบค่าคงที่ก่อนการประเมิน0x20และ0x30eในการส่งออกประกอบ


คุณจะไม่ต้องการif (x==c) foo(c) else foo(x)? หากเพียงเพื่อจับconstexprการใช้งานของfoo?
MSalters

@Malters: ฉันรู้ว่ามีคนจะถามว่า !! ฉันมากับเทคนิคนี้มาก่อนconstexprเป็นสิ่งหนึ่งและไม่เคยใส่ใจที่จะ "อัพเดท" หลังจากนั้น (แม้ว่าฉันจะไม่ได้กังวลมากนักเกี่ยวกับconstexprหลังจากนั้น) แต่เหตุผลที่ฉันไม่ได้ทำตอนแรกก็คือฉันต้องการ ทำให้ง่ายขึ้นสำหรับคอมไพเลอร์แยกออกเป็นรหัสทั่วไปและลบสาขาหากตัดสินใจที่จะปล่อยพวกเขาเป็นวิธีการโทรปกติและไม่ปรับ ฉันคาดว่าถ้าฉันใส่cมันยากจริงๆสำหรับคอมไพเลอร์ถึง c (ขออภัยตลกไม่ดี) ว่าทั้งสองเป็นรหัสเดียวกันแม้ว่าฉันจะไม่เคยตรวจสอบเรื่องนี้
user541686

4

ฉันแค่ขว้างเพื่อบอกว่าถ้าคุณต้องการโซลูชันที่มีมาตรฐานมากกว่า C ++ คุณสามารถใช้[[noreturn]]คุณลักษณะเพื่อเขียนของคุณเองunreachableแอตทริบิวต์การเขียนของคุณเอง

ดังนั้นฉันจะ re-purpose ปฏิเสธตัวอย่างที่ยอดเยี่ยมเพื่อแสดง:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

สิ่งที่คุณเห็นผลลัพธ์ในรหัสเกือบเหมือนกัน:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

แน่นอนว่าข้อเสียคือคุณจะได้รับคำเตือนว่า[[noreturn]]ฟังก์ชั่นทำหน้าที่ส่งคืน


จะทำงานร่วมกับclang, เมื่อการแก้ปัญหาเดิมของฉันไม่ได้เคล็ดลับที่ดีมากและ +1 แต่สิ่งทั้งหมดนั้นขึ้นอยู่กับคอมไพเลอร์มาก (ดังที่ Peter Cordes แสดงให้เราเห็นว่าiccมันอาจทำให้ประสิทธิภาพแย่ลง ) ดังนั้นมันจึงยังไม่สามารถใช้ได้ในระดับสากล นอกจากนี้หมายเหตุเล็กน้อย: ต้องมีunreachableคำจำกัดความสำหรับเครื่องมือเพิ่มประสิทธิภาพและอินไลน์เพื่อให้สามารถใช้งานได้
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.