ใช้งานจริงของคำหลัก 'จำกัด ' C99?


183

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

มีใครบ้างที่สามารถเสนอกรณีที่เป็นจริงได้ซึ่งคุ้มค่ากับการใช้งานจริงหรือไม่?


4
memcpyvs memmoveเป็นตัวอย่างที่ยอมรับได้หนึ่งตัวอย่าง
Alexandre C.

@AlexandreC: ฉันไม่คิดว่ามันเป็นคุณสมบัติที่เหมาะสมเป็นพิเศษเนื่องจากการขาดคุณสมบัติ "จำกัด " ไม่ได้หมายความว่าตรรกะของโปรแกรมจะทำงานกับแหล่งที่มาและปลายทางที่มีการโหลดมากเกินไปและจะไม่มีคุณสมบัติดังกล่าวป้องกันวิธีที่เรียก การพิจารณาว่าแหล่งที่มาและปลายทางทับซ้อนกันหรือไม่และหากเป็นเช่นนั้นการแทนที่ dest ด้วย src + (dest-src) ซึ่งเนื่องจากมาจาก src จะได้รับอนุญาตให้ใช้นามแฝง
supercat

@supercat: นั่นเป็นเหตุผลที่ฉันใส่มันเป็นความคิดเห็น อย่างไรก็ตาม 1) restrict-qualifying ข้อโต้แย้งที่จะmemcpyช่วยในหลักการการดำเนินงานที่ไร้เดียงสาที่จะเพิ่มประสิทธิภาพในเชิงรุกและ 2) เพียงโทรmemcpyช่วยให้คอมไพเลอร์ที่จะคิดว่าการขัดแย้งให้กับมันไม่ได้มีนามแฝงซึ่งอาจทำให้การเพิ่มประสิทธิภาพบางรอบmemcpyโทร
Alexandre C.

@AlexandreC: มันจะยากสำหรับคอมไพเลอร์ในแพลตฟอร์มส่วนใหญ่เพื่อเพิ่มประสิทธิภาพ memcpy ไร้เดียงสา - แม้จะมี "จำกัด " - ที่ใดก็ได้ที่มีประสิทธิภาพใกล้เคียงกับรุ่นที่เหมาะกับเป้าหมาย การปรับให้เหมาะสมที่สุดสำหรับการโทรจะไม่ต้องการคำหลัก "จำกัด " และในบางกรณีความพยายามในการอำนวยความสะดวกให้กับผู้ใช้นั้นอาจมีผลในทางตรงกันข้าม ยกตัวอย่างเช่นการใช้งานหลาย memcpy สามารถที่ศูนย์ค่าใช้จ่ายพิเศษเรื่องmemcpy(anything, anything, 0);เป็นไม่มี-op และให้แน่ใจว่าถ้าpเป็นตัวชี้อย่างน้อยnไบต์สามารถเขียนmemcpy(p,p,n); จะไม่มีผลข้างเคียงที่ไม่พึงประสงค์ กรณีดังกล่าวอาจจะเกิดขึ้น ...
SuperCat

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

คำตอบ:


182

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

ตัวอย่างเช่นสมมติว่าฉันมีเครื่องที่มีคำแนะนำพิเศษที่สามารถคูณจำนวนเวกเตอร์ในหน่วยความจำและฉันมีรหัสต่อไปนี้:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

คอมไพเลอร์ต้องจัดการอย่างถูกต้องหากdest, src1และsrc2ทับซ้อนกันซึ่งหมายความว่าจะต้องทำการคูณครั้งละหนึ่งครั้งตั้งแต่ต้นจนจบ โดยการมีrestrictคอมไพเลอร์มีอิสระที่จะเพิ่มประสิทธิภาพรหัสนี้โดยใช้คำแนะนำเวกเตอร์

วิกิพีเดียมีรายการบนrestrictมีอีกตัวอย่างหนึ่งที่นี่


3
@Michael - หากฉันไม่เข้าใจผิดปัญหาจะเกิดขึ้นก็ต่อเมื่อdestเหลื่อมกันเวกเตอร์แหล่งใดแหล่งหนึ่ง ทำไมจะมีปัญหาหากsrc1และsrc2ทับซ้อนกัน?
ysap

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

การเพิ่มคีย์เวิร์ด register ลงในตัวแปร for loop ยังทำให้เร็วขึ้นนอกเหนือจากการเพิ่มข้อ จำกัด

2
จริงๆแล้วคีย์เวิร์ด register เป็นคำแนะนำเท่านั้น และในคอมไพเลอร์ตั้งแต่ประมาณปี 2000 i (และ n สำหรับการเปรียบเทียบ) ในตัวอย่างจะถูกปรับให้เหมาะกับการลงทะเบียนไม่ว่าคุณจะใช้คีย์เวิร์ด register หรือไม่
Mark Fischler

154

วิกิพีเดียตัวอย่างเป็นมากส่องสว่าง

มันแสดงให้เห็นอย่างชัดเจนว่าจะช่วยให้การบันทึกการเรียนการสอนการประกอบหนึ่ง

โดยไม่ จำกัด :

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

ประกอบหลอก:

load R1  *x    ; Load the value of x pointer
load R2  *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2  *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

ด้วยข้อ จำกัด :

void fr(int *restrict a, int *restrict b, int *restrict x);

ประกอบหลอก:

load R1  *x
load R2  *a
add R2 += R1
set R2  *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2  *b
add R2 += R1
set R2  *b

GCC ทำจริงหรือ

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

ด้วย-O0พวกเขาเหมือนกัน

ด้วย-O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

สำหรับการฝึกหัดการเรียกคือ:

  • rdi = พารามิเตอร์แรก
  • rsi = พารามิเตอร์ที่สอง
  • rdx = พารามิเตอร์ที่สาม

ผลลัพธ์ของ GCC นั้นชัดเจนยิ่งกว่าบทความ wiki: 4 คำแนะนำกับ 3 คำแนะนำ

อาร์เรย์

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

ลองพิจารณาตัวอย่าง:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

เนื่องจากrestrictคอมไพเลอร์สมาร์ท (หรือมนุษย์) สามารถเพิ่มประสิทธิภาพให้กับ:

memset(p1, 4, 50);
memset(p2, 9, 50);

ซึ่งอาจมีประสิทธิภาพมากขึ้นเนื่องจากอาจมีการเพิ่มประสิทธิภาพการประกอบในการใช้งาน libc ที่เหมาะสม (เช่น glibc): มันจะดีกว่าถ้าใช้ std :: memcpy () หรือ std :: copy () ในแง่ของประสิทธิภาพ?

GCC ทำจริงหรือ

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

ด้วย-O0ทั้งคู่เหมือนกัน

ด้วย-O3:

  • ด้วยข้อ จำกัด :

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    สองmemsetสายตามที่คาดไว้

  • โดยไม่ จำกัด : ไม่มี STDLIB สายเพียง 16 ย้ำกว้างห่วงคลี่ซึ่งฉันไม่ได้ตั้งใจที่จะทำซ้ำที่นี่ :-)

ฉันไม่ได้มีความอดทนในการเปรียบเทียบพวกเขา แต่ฉันเชื่อว่าการ จำกัด เวอร์ชันจะเร็วขึ้น

C99

ลองดูที่มาตรฐานเพื่อความสมบูรณ์

restrictบอกว่าตัวชี้สองตัวไม่สามารถชี้ไปที่ส่วนของหน่วยความจำที่ทับซ้อนกันได้ การใช้งานทั่วไปส่วนใหญ่มีไว้สำหรับอาร์กิวเมนต์ของฟังก์ชัน

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

หากผู้โทรไม่ปฏิบัติตามrestrictสัญญาให้ระบุพฤติกรรมที่ไม่ได้กำหนดไว้

C99 N1256 ร่าง 6.7.3 / 7 "ชนิดบ่น" พูดว่า:

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

6.7.3.1 "การจำกัดความหมายอย่างเป็นทางการ" ให้รายละเอียดเต็มไปด้วยเลือด

กฎนามแฝงที่เข้มงวด

restrictคำหลักที่จะมีผลต่อตัวชี้ประเภทที่เข้ากันได้ (เช่นสองint*) เพราะกฎระเบียบที่เข้มงวด aliasing บอกว่า aliasing ชนิดเข้ากันไม่ได้เป็นพฤติกรรมที่ไม่ได้กำหนดโดยค่าเริ่มต้นและเพื่อให้คอมไพเลอร์สามารถสันนิษฐานได้ว่ามันไม่ได้เกิดขึ้นและเพิ่มประสิทธิภาพออกไป

ดู: กฎนามแฝงที่เข้มงวดคืออะไร

ดูสิ่งนี้ด้วย


9
ตัวระบุ "จำกัด " สามารถช่วยให้คุณประหยัดได้มากขึ้น ตัวอย่างเช่นที่กำหนดตัวระบุvoid zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }ที่ จำกัด จะอนุญาตให้คอมไพเลอร์เขียนโค้ดใหม่เป็น "memset (p1,4,50); memset (p2,9,50);" ข้อ จำกัด นั้นเหนือกว่าการใช้นามแฝงประเภทฐาน มันเป็นคอมไพเลอร์อัปยศที่เน้นไปที่ส่วนหลัง
supercat

@supercat เป็นตัวอย่างที่ดีเพิ่มเข้ามาเพื่อตอบ
Ciro Santilli 法轮功冠状病六四事件法轮功

2
@ tim18: คำหลัก "จำกัด " สามารถเปิดใช้งานการปรับแต่งมากมายที่แม้แต่การเพิ่มประสิทธิภาพตามประเภทก้าวร้าวก็ไม่สามารถทำได้ นอกจากนี้การมีอยู่ของ "จำกัด " ในภาษา - แตกต่างจากการใช้นามแฝงประเภทก้าวร้าว - ไม่เคยทำให้มันเป็นไปไม่ได้ที่จะทำงานให้สำเร็จอย่างมีประสิทธิภาพเท่าที่จะเป็นไปได้ในกรณีที่ไม่มี (ตั้งแต่รหัสซึ่งจะถูกทำลายโดย "จำกัด " อย่าใช้มันในขณะที่โค้ดที่ถูกทำลายโดย TBAA ที่ก้าวร้าวจะต้องเขียนใหม่ด้วยวิธีที่มีประสิทธิภาพน้อยกว่า)
supercat

2
@ tim18: สิ่งเซอร์ราวด์ที่มีสองครั้งที่ขีดเส้นใต้ใน backticks __restrictในขณะที่ มิฉะนั้นการขีดเส้นใต้คู่อาจตีความผิดเป็นตัวบ่งชี้ว่าคุณกำลังตะโกน
supercat

1
สิ่งที่สำคัญกว่าการไม่ตะโกนก็คือสิ่งที่ขีดเส้นใต้มีความหมายที่เกี่ยวข้องโดยตรงกับประเด็นที่คุณพยายามทำ
remveland
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.