มีค่าใช้จ่ายในการประกาศตัวแปรภายในลูปหรือไม่? (C ++)


158

ฉันแค่สงสัยว่าจะมีการสูญเสียความเร็วหรือประสิทธิภาพหรือไม่หากคุณทำสิ่งนี้:

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

ซึ่งประกาศint varหนึ่งร้อยครั้ง สำหรับฉันดูเหมือนว่าจะมี แต่ฉันไม่แน่ใจ การทำสิ่งนี้แทนจะทำได้จริงหรือเร็วกว่านี้หรือไม่:

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

หรือเหมือนกันอย่างรวดเร็วและมีประสิทธิภาพ?


7
เพื่อความชัดเจนรหัสข้างต้นไม่ได้ "ประกาศ" var หนึ่งร้อยครั้ง
jason

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

2
@jason หากตัวอย่างแรกของโค้ดไม่ประกาศตัวแปร 'var' หนึ่งร้อยครั้งคุณสามารถอธิบายสิ่งที่เกิดขึ้นได้หรือไม่? มันประกาศตัวแปรเพียงครั้งเดียวและเริ่มต้น 100 ครั้งหรือไม่? ฉันคิดว่ารหัสประกาศและเริ่มต้นตัวแปร 100 ครั้งเนื่องจากทุกอย่างในลูปถูกดำเนินการ 100 ครั้ง ขอบคุณ.
randomUser47534

คำตอบ:


194

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


50
ฉันหวังว่าคนที่สอนนอกวิทยาลัยอย่างน้อยก็รู้เรื่องพื้นฐานนี้ ครั้งหนึ่งเขาหัวเราะเยาะฉันที่ประกาศตัวแปรในลูปและฉันก็สงสัยว่ามีอะไรผิดปกติจนกระทั่งเขาอ้างถึงการแสดงเป็นเหตุผลที่จะไม่ทำเช่นนั้นและฉันก็ชอบ "WTF!?"
mmx

18
คุณแน่ใจหรือไม่ว่าคุณควรพูดถึงพื้นที่สแต็กทันที ตัวแปรเช่นนี้อาจอยู่ในรีจิสเตอร์
toto

3
@toto ตัวแปรเช่นนี้อาจไม่มีที่ไหนเลย - varตัวแปรจะเริ่มต้น แต่ไม่เคยใช้ดังนั้นเครื่องมือเพิ่มประสิทธิภาพที่เหมาะสมจึงสามารถลบออกได้ทั้งหมด (ยกเว้นตัวอย่างข้อมูลที่สองหากตัวแปรถูกใช้ที่ใดที่หนึ่งหลังจากลูป)
CiaPan

@Mehrdad Afshari ตัวแปรในลูปได้รับตัวสร้างที่เรียกว่าหนึ่งครั้งต่อการวนซ้ำ แก้ไข - ฉันเห็นคุณพูดถึงสิ่งนี้ด้านล่าง แต่ฉันคิดว่ามันสมควรได้รับการกล่าวถึงในคำตอบที่ยอมรับเช่นกัน
hoodaticus

106

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

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


42
แก้ไขความคิดผิดเหตุผล ตัวแปรนอกลูป สร้างครั้งเดียวทำลายครั้งเดียว แต่ตัวดำเนินการ asignment ใช้การทำซ้ำทุกครั้ง ตัวแปรภายในลูป Constructe / Desatructor อธิบายการทำซ้ำทุกครั้ง แต่การดำเนินการกำหนดเป็นศูนย์
Martin York

8
นี่เป็นคำตอบที่ดีที่สุด แต่ความคิดเห็นเหล่านี้ทำให้สับสน มีความแตกต่างอย่างมากระหว่างการเรียกตัวสร้างและตัวดำเนินการมอบหมาย
Andrew Grant

1
มันเป็นเรื่องจริงถ้า loop body ทำการกำหนดไม่ใช่แค่สำหรับการเริ่มต้นเท่านั้น และหากมีเพียงการเริ่มต้นที่ไม่ขึ้นกับร่างกาย / คงที่เครื่องมือเพิ่มประสิทธิภาพสามารถยกได้
peterchen

7
@ แอนดรูว์แกรนท์: ทำไม. โดยปกติตัวดำเนินการกำหนดจะถูกกำหนดให้เป็น copy สร้างลงใน tmp ตามด้วย swap (เพื่อให้ปลอดภัยยกเว้น) ตามด้วย destroy tmp ดังนั้นผู้ดำเนินการที่ได้รับมอบหมายจึงไม่แตกต่างจากวงจรการก่อสร้าง / ทำลายข้างต้น ดูstackoverflow.com/questions/255612/…เช่นตัวดำเนินการกำหนดทั่วไป
Martin York

1
หากการสร้าง / ทำลายมีราคาแพงต้นทุนรวมของพวกเขาจะเป็นขีด จำกัด สูงสุดที่สมเหตุสมผลสำหรับต้นทุนของตัวดำเนินการ = แต่งานมอบหมายอาจถูกกว่า นอกจากนี้ในขณะที่เราขยายการสนทนานี้จากประเภท ints เป็น C ++ เราสามารถสรุป 'var = 4' เป็นการดำเนินการอื่นที่ไม่ใช่ 'กำหนดตัวแปรจากค่าประเภทเดียวกัน'
greggo

69

ทั้งคู่เหมือนกันและนี่คือวิธีที่คุณสามารถค้นหาได้โดยดูว่าคอมไพเลอร์ทำอะไร (แม้ว่าจะไม่ได้ตั้งค่าการเพิ่มประสิทธิภาพไว้ที่สูงก็ตาม):

ดูว่าคอมไพเลอร์ (gcc 4.0) ทำอะไรกับตัวอย่างง่ายๆของคุณ:

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1. วินาที:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2. ค

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2. วินาที:

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

จากสิ่งเหล่านี้คุณจะเห็นสองสิ่ง: ประการแรกรหัสเหมือนกันทั้งสองอย่าง

ประการที่สองพื้นที่เก็บข้อมูลสำหรับ var ถูกจัดสรรนอกลูป:

         subl    $24, %esp

และในที่สุดสิ่งเดียวในลูปคือการมอบหมายและการตรวจสอบเงื่อนไข:

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

ซึ่งมีประสิทธิภาพมากที่สุดเท่าที่คุณจะทำได้โดยไม่ต้องถอดลูปออกทั้งหมด


2
"ซึ่งมีประสิทธิภาพมากที่สุดเท่าที่คุณจะทำได้โดยไม่ต้องถอดลูปออกทั้งหมด" ไม่มาก การคลายลูปบางส่วน (พูดว่า 4 ครั้งต่อรอบ) จะทำให้มันเร็วขึ้นอย่างมาก อาจมีวิธีอื่น ๆ อีกมากมายในการปรับให้เหมาะสม ... ถ้าใช้ 'i' ในภายหลังก็แค่ตั้งค่า 'i' = 100
darron

นั่นคือสมมติว่ารหัสเปลี่ยนเป็น 'i' ที่เพิ่มขึ้นเลย ... เหมือนเดิมตลอดไป
darron

เช่นเดียวกับโพสต์เดิม!
Alex Brown

2
ฉันชอบคำตอบที่กลับทฤษฎีพร้อมการพิสูจน์! ยินดีที่ได้เห็น ASM Dump สำรองข้อมูลทฤษฎีการเป็นรหัสที่เท่ากัน +1
Xavi Montero

1
ฉันสร้างผลลัพธ์โดยการสร้างรหัสเครื่องสำหรับแต่ละเวอร์ชัน ไม่จำเป็นต้องเรียกใช้
Alex Brown

14

วันนี้จะเป็นการดีกว่าที่จะประกาศภายในลูปเว้นแต่จะเป็นค่าคงที่เนื่องจากคอมไพเลอร์จะสามารถปรับโค้ดให้เหมาะสมได้ดีขึ้น (ลดขอบเขตตัวแปร)

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


4
ฉันสงสัยว่ามันจะส่งผลต่อการปรับให้เหมาะสม - หากคอมไพลเลอร์ทำการวิเคราะห์โฟลว์ข้อมูลประเภทใด ๆ ก็สามารถคิดได้ว่าไม่มีการแก้ไขนอกลูปดังนั้นจึงควรสร้างโค้ดที่ปรับให้เหมาะสมเหมือนกันในทั้งสองกรณี
Adam Rosenfield

3
จะไม่ทราบว่าคุณมีสองลูปที่แตกต่างกันโดยใช้ชื่อตัวแปร temp เดียวกันหรือไม่
Joshua

11

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


3
ฉันไม่นับว่าเป็นการเพิ่มประสิทธิภาพจริงๆ เนื่องจากเป็นตัวแปรภายในพื้นที่สแต็กจึงถูกจัดสรรที่จุดเริ่มต้นของฟังก์ชัน ไม่มี "การสร้าง" ที่แท้จริงที่เกี่ยวข้องกับการแสดงที่เป็นอันตราย (เว้นแต่จะมีการเรียกตัวสร้างซึ่งเป็นอีกเรื่องหนึ่งโดยสิ้นเชิง)
mmx

คุณพูดถูก "การเพิ่มประสิทธิภาพ" เป็นคำที่ผิด แต่ฉันยอมแพ้เพื่อสิ่งที่ดีกว่า
Andrew Hare

ปัญหาคือเครื่องมือเพิ่มประสิทธิภาพดังกล่าวจะใช้การวิเคราะห์ช่วงสดและตัวแปรทั้งสองค่อนข้างตาย
MSalters

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

9

สำหรับประเภทในตัวอาจจะไม่มีความแตกต่างระหว่าง 2 สไตล์ (อาจจะลงไปถึงโค้ดที่สร้างขึ้น)

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

คลาส RAII อาจต้องการพฤติกรรมนี้ ตัวอย่างเช่นคลาสที่จัดการอายุการเข้าถึงไฟล์อาจต้องสร้างและทำลายในการวนซ้ำแต่ละครั้งเพื่อจัดการการเข้าถึงไฟล์อย่างถูกต้อง

สมมติว่าคุณมีLockMgrคลาสที่ได้รับส่วนสำคัญเมื่อสร้างและเผยแพร่เมื่อถูกทำลาย:

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

ค่อนข้างแตกต่างจาก:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

6

ลูปทั้งสองมีประสิทธิภาพเท่ากัน พวกเขาทั้งสองจะใช้เวลาไม่สิ้นสุด :) อาจเป็นความคิดที่ดีที่จะเพิ่ม i ภายในลูป


อ่าใช่ฉันลืมที่จะกล่าวถึงประสิทธิภาพของพื้นที่ - ไม่เป็นไร - 2 ints สำหรับทั้งคู่ ดูเหมือนว่าเป็นเรื่องแปลกสำหรับฉันที่โปรแกรมเมอร์ไม่มีป่าสำหรับต้นไม้ - คำแนะนำทั้งหมดนี้เกี่ยวกับโค้ดบางตัวที่ไม่ยุติ
Larry Watanabe

ไม่เป็นไรหากพวกเขาไม่ยุติ ไม่มีใครเรียก :-)
Nosredna

2

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


ความแตกต่างอาจมาจากความแตกต่างในขอบเขต ยิ่งขอบเขตเล็กลงเท่าใดคอมไพเลอร์ก็สามารถกำจัดการทำให้เป็นอนุกรมของตัวแปรได้มากขึ้นเท่านั้น ในขอบเขตวงเล็กตัวแปรน่าจะถูกใส่ไว้ในรีจิสเตอร์และไม่ได้บันทึกไว้ในสแต็กเฟรม หากคุณเรียกใช้ฟังก์ชันในลูปหรือหักล้างตัวชี้ที่คอมไพลเลอร์ไม่ทราบว่ามันชี้ไปที่ใดมันจะทำให้ตัวแปรลูปหกหากอยู่ในขอบเขตฟังก์ชัน (ตัวชี้อาจมี&i)
Patrick Schlüter

กรุณาโพสต์การตั้งค่าและผลลัพธ์ของคุณ
jxramos

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

โค้ดด้านบนจะพิมพ์ 100 10 ครั้งเสมอซึ่งหมายความว่าตัวแปรภายในลูปจะถูกจัดสรรเพียงครั้งเดียวต่อการเรียกใช้ฟังก์ชันแต่ละครั้ง


0

วิธีเดียวที่จะมั่นใจได้คือเวลาพวกเขา แต่ความแตกต่างถ้ามีจะเป็นกล้องจุลทรรศน์ดังนั้นคุณจะต้องมีวงเวลาขนาดใหญ่ที่ยิ่งใหญ่

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


"วิธีเดียวที่จะมั่นใจได้คือการกำหนดเวลา" -1 ไม่จริง ขออภัยโพสต์อื่นพิสูจน์ว่าสิ่งนี้ผิดโดยการเปรียบเทียบภาษาเครื่องที่สร้างขึ้นและพบว่ามันเหมือนกันเป็นหลัก ฉันไม่มีปัญหากับคำตอบของคุณโดยทั่วไป แต่ไม่ผิดสิ่งที่ -1 คืออะไร?
Bill K

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

-1

คอมไพเลอร์มีแนวโน้มที่จะกำหนดรีจิสเตอร์สำหรับทั้งสองตัวแปร การลงทะเบียนเหล่านี้ยังคงมีอยู่ดังนั้นจึงไม่ต้องใช้เวลา มี 2 ​​register write และ one register read instruction ในทั้งสองกรณี


-2

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


-6

นั่นไม่เป็นความจริงมีค่าใช้จ่ายอย่างไรก็ตามการละเลยสามารถอยู่เหนือศีรษะได้

แม้ว่าอาจจะจบลงที่ตำแหน่งเดียวกันในสแต็ก แต่ก็ยังกำหนดให้ มันจะกำหนดตำแหน่งหน่วยความจำบนสแต็กสำหรับ int นั้นจากนั้นจึงว่างเมื่อสิ้นสุด} ไม่ได้อยู่ในความรู้สึกที่ไม่มีฮีปมันจะย้าย sp (ตัวชี้สแต็ก) ทีละ 1 และในกรณีของคุณเมื่อพิจารณาว่ามันมีตัวแปรท้องถิ่นเพียงตัวเดียวมันก็จะเท่ากับ fp (ตัวชี้เฟรม) และ sp

คำตอบสั้น ๆ คือ: อย่าดูแลอย่างใดอย่างหนึ่งวิธีการทำงานเกือบเหมือนกัน

แต่ลองอ่านเพิ่มเติมเกี่ยวกับการจัดเรียงสแต็ก โรงเรียนระดับปริญญาตรีของฉันมีการบรรยายที่ดีเกี่ยวกับเรื่องนั้นหากคุณต้องการอ่านเพิ่มเติมโปรดดูที่นี่ http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html


อีกครั้ง -1 ไม่จริง อ่านกระทู้ที่ดูการชุมนุม
Bill K

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