มีความแตกต่างของประสิทธิภาพระหว่าง i ++ และ ++ i ใน C ++ หรือไม่


352

เรามีคำถามคือมีความแตกต่างระหว่างประสิทธิภาพi++และ++i ใน C ?

คำตอบสำหรับ C ++ คืออะไร


ฉัน retagged เนื่องจากทั้งสองแท็กเป็นวิธีที่ง่ายที่สุดในการค้นหาคำถามในลักษณะนี้ ฉันผ่านคนอื่นที่ไม่มีแท็กเหนียวและให้แท็กเหนียว
George Stocker

104
มีความแตกต่างด้านประสิทธิภาพระหว่างการใช้ C ++ และ ++ C หรือไม่
new123456

2
บทความ: มันมีเหตุผลที่จะใช้ตัวดำเนินการเพิ่มคำนำหน้า ++ มันแทนตัวดำเนินการ postfix มัน ++ สำหรับตัววนซ้ำ? - viva64.com/th/b/0093

คำตอบ:


426

[บทสรุปผู้บริหาร: ใช้++iหากคุณไม่มีเหตุผลเฉพาะที่จะใช้i++]

สำหรับ C ++ คำตอบนั้นซับซ้อนกว่าเล็กน้อย

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

อย่างไรก็ตามถ้าiเป็นอินสแตนซ์ของคลาส C ++ ดังนั้นi++และ++iกำลังเรียกไปยังหนึ่งในoperator++ฟังก์ชัน นี่คือฟังก์ชันมาตรฐานคู่เหล่านี้:

Foo& Foo::operator++()   // called for ++i
{
    this->data += 1;
    return *this;
}

Foo Foo::operator++(int ignored_dummy_value)   // called for i++
{
    Foo tmp(*this);   // variable "tmp" cannot be optimized away by the compiler
    ++(*this);
    return tmp;
}

เนื่องจากคอมไพเลอร์ไม่ได้สร้างรหัส แต่เพียงเรียกใช้operator++ฟังก์ชันจึงไม่มีวิธีที่จะปรับtmpตัวแปรให้เหมาะสมและตัวสร้างสำเนาที่เกี่ยวข้อง หากตัวสร้างสำเนามีราคาแพงแสดงว่ามีผลกระทบต่อประสิทธิภาพอย่างมาก


3
สิ่งที่คอมไพเลอร์สามารถหลีกเลี่ยงได้คือสำเนาที่สองเพื่อส่งคืน tmp โดยจัดสรร tmp ในตัวเรียกผ่าน NRVO ตามที่กล่าวถึงโดยความคิดเห็นอื่น
Blaisorblade

7
คอมไพเลอร์ไม่สามารถหลีกเลี่ยงปัญหานี้พร้อมกันได้หรือไม่หากตัวดำเนินการ ++ อยู่ในบรรทัด
Eduard - Gabriel Munteanu

16
ใช่ถ้าตัวดำเนินการ ++ เป็นแบบอินไลน์และไม่เคยใช้ tmp มันสามารถลบออกได้ยกเว้นว่าตัวสร้างหรือวัตถุ destructor ของวัตถุ tmp มีผลข้างเคียง
Zan Lynx

5
@kriss: ความแตกต่างระหว่าง C และ C ++ คือใน C คุณมีการรับประกันว่าผู้ประกอบการจะได้รับการ inline และ ณ จุดนั้นเครื่องมือเพิ่มประสิทธิภาพที่ดีจะสามารถลบความแตกต่าง; แทนใน C ++ คุณไม่สามารถสรุปแบบอินไลน์ได้ - ไม่เสมอไป
Blaisorblade

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

64

ใช่. นั่นคือ

ตัวดำเนินการ ++ อาจกำหนดหรือไม่ก็ได้ว่าเป็นฟังก์ชั่น สำหรับประเภทดึกดำบรรพ์ (int, double, ... ) ตัวดำเนินการถูกสร้างขึ้นดังนั้นคอมไพเลอร์อาจจะสามารถปรับรหัสของคุณให้เหมาะสม แต่ในกรณีของวัตถุที่กำหนดสิ่งที่ผู้ประกอบการ ++ จะแตกต่างกัน

ฟังก์ชันโอเปอเรเตอร์ ++ (int) จะต้องสร้างสำเนา นั่นเป็นเพราะ postfix ++ คาดว่าจะส่งคืนค่าที่แตกต่างจากที่เก็บไว้: จะต้องเก็บค่าไว้ในตัวแปร temp เพิ่มค่าและส่งคืน temp ในกรณีของโอเปอเรเตอร์ ++ ++ (), คำนำหน้า ++ ไม่จำเป็นต้องสร้างสำเนา: วัตถุสามารถเพิ่มตัวมันเองแล้วกลับมาเอง

นี่คือภาพประกอบของประเด็น:

struct C
{
    C& operator++();      // prefix
    C  operator++(int);   // postfix

private:

    int i_;
};

C& C::operator++()
{
    ++i_;
    return *this;   // self, no copy created
}

C C::operator++(int ignored_dummy_value)
{
    C t(*this);
    ++(*this);
    return t;   // return a copy
}

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


2
"ตัวดำเนินการเพิ่มค่าก่อนนำเสนอการพึ่งพาข้อมูลในรหัส: CPU ต้องรอให้การดำเนินการเพิ่มเสร็จสมบูรณ์ก่อนที่จะสามารถใช้ค่าในนิพจน์ได้บน CPU ที่มีการประมวลผลอย่างลึกไปป์ไลน์นี้จะแนะนำแผงลอย สำหรับตัวดำเนินการเพิ่มภายหลัง " ( Game Engine Architecture (รุ่นที่ 2) ) ดังนั้นหากการคัดลอกการเพิ่มโพสต์นั้นไม่ได้มีความเข้มข้นในการคำนวณมันก็ยังสามารถเอาชนะการเพิ่มขึ้นก่อนได้
Matthias

ในรหัส postfix วิธีนี้ทำงานC t(*this); ++(*this); return t;ในบรรทัดที่สองคุณกำลังเพิ่มตัวชี้นี้ให้ถูกต้องดังนั้นจะtได้รับการอัปเดตอย่างไรถ้าคุณเพิ่มส่วนนี้ ค่าของสิ่งนี้ถูกคัดลอกไปแล้วtหรือไม่?
rasen58

The operator++(int) function must create a copy.ไม่มันไม่ใช่. ไม่มากไปกว่าสำเนาoperator++()
Severin Pappadeux

47

นี่คือมาตรฐานสำหรับกรณีที่เมื่อผู้ประกอบการเพิ่มขึ้นอยู่ในหน่วยการแปลที่แตกต่างกัน คอมไพเลอร์กับ g ++ 4.5

ไม่สนใจปัญหาสไตล์ในตอนนี้

// a.cc
#include <ctime>
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};

int main () {
    Something s;

    for (int i=0; i<1024*1024*30; ++i) ++s; // warm up
    std::clock_t a = clock();
    for (int i=0; i<1024*1024*30; ++i) ++s;
    a = clock() - a;

    for (int i=0; i<1024*1024*30; ++i) s++; // warm up
    std::clock_t b = clock();
    for (int i=0; i<1024*1024*30; ++i) s++;
    b = clock() - b;

    std::cout << "a=" << (a/double(CLOCKS_PER_SEC))
              << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n';
    return 0;
}

เพิ่มขึ้น O (n)

ทดสอบ

// b.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    for (auto it=data.begin(), end=data.end(); it!=end; ++it)
        ++*it;
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

ผล

ผลลัพธ์ (การกำหนดเวลาเป็นวินาที) ด้วย g ++ 4.5 บนเครื่องเสมือน:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      1.70  2.39
-DPACKET_SIZE=50 -O3      0.59  1.00
-DPACKET_SIZE=500 -O1    10.51 13.28
-DPACKET_SIZE=500 -O3     4.28  6.82

เพิ่มขึ้น O (1)

ทดสอบ

ให้เราใช้ไฟล์ต่อไปนี้:

// c.cc
#include <array>
class Something {
public:
    Something& operator++();
    Something operator++(int);
private:
    std::array<int,PACKET_SIZE> data;
};


Something& Something::operator++()
{
    return *this;
}

Something Something::operator++(int)
{
    Something ret = *this;
    ++*this;
    return ret;
}

มันไม่ทำอะไรเลยในการเพิ่ม สิ่งนี้จะจำลองกรณีเมื่อการเพิ่มมีความซับซ้อนคงที่

ผล

ผลลัพธ์ตอนนี้แตกต่างกันมาก:

Flags (--std=c++0x)       ++i   i++
-DPACKET_SIZE=50 -O1      0.05   0.74
-DPACKET_SIZE=50 -O3      0.08   0.97
-DPACKET_SIZE=500 -O1     0.05   2.79
-DPACKET_SIZE=500 -O3     0.08   2.18
-DPACKET_SIZE=5000 -O3    0.07  21.90

ข้อสรุป

ผลการดำเนินงานที่ชาญฉลาด

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

ความหมายที่ชาญฉลาด

  • i++increment i, I am interested in the previous value, thoughกล่าวว่า
  • ++iกล่าวหรือincrement i, I am interested in the current value increment i, no interest in the previous valueอีกครั้งคุณจะคุ้นเคยกับมันแม้ว่าคุณจะไม่ได้ตอนนี้

นู

การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วร้ายทั้งหมด เช่นเดียวกับการมองในแง่ร้ายก่อนวัยอันควร


1
การทดสอบที่น่าสนใจ ตอนนี้เกือบสองปีครึ่ง gcc 4.9 และ Clang 3.4 แสดงแนวโน้มที่คล้ายกัน เสียงดังกราวนั้นเร็วกว่าทั้งคู่ แต่ความแตกต่างระหว่าง pre และ postfix นั้นแย่กว่า gcc
ถุงเท้าเคี้ยว

สิ่งที่ฉันอยากเห็นจริง ๆ เป็นตัวอย่างในโลกแห่งความเป็นจริงที่ ++ i / i ++ สร้างความแตกต่าง ตัวอย่างเช่นมันสร้างความแตกต่างให้กับตัววนซ้ำมาตรฐานหรือไม่?
Jakob Schou Jensen

@JakobSchouJensen: สิ่งเหล่านี้ตั้งใจให้เป็นตัวอย่างในโลกแห่งความเป็นจริง พิจารณาแอปพลิเคชันขนาดใหญ่ที่มีโครงสร้างต้นไม้ที่ซับซ้อน (เช่น kd-trees, quad-trees) หรือคอนเทนเนอร์ขนาดใหญ่ที่ใช้ในเทมเพลตนิพจน์ (เพื่อเพิ่มปริมาณข้อมูลบนฮาร์ดแวร์ SIMD) ถ้ามันสร้างความแตกต่างที่นั่นฉันไม่แน่ใจว่าทำไมคนเราถึงเลือกที่จะโพสต์เพิ่มขึ้นในบางกรณีหากไม่จำเป็นต้องใช้ความหมาย
เซบาสเตียนมัค

@phresnel: ฉันไม่คิดว่าโอเปอเรเตอร์ ++ จะเป็นเทมเพลตนิพจน์ในชีวิตประจำวันของคุณคุณมีตัวอย่างจริงหรือไม่? การใช้งานทั่วไปของโอเปอเรเตอร์ ++ นั้นเป็นจำนวนเต็มและตัววนซ้ำ ฉันคิดว่ามันน่าสนใจที่จะทราบว่ามีความแตกต่างใด ๆ (ไม่มีความแตกต่างของจำนวนเต็มแน่นอน - แต่เป็นตัววนซ้ำ)
Jakob Schou Jensen

@JakobSchouJensen: ไม่มีตัวอย่างธุรกิจจริง ๆ แต่มีแอปพลิเคชั่นจำนวนหนึ่งที่คุณต้องการนับจำนวน Wrt iterators ลองพิจารณา ray tracer ที่เขียนในสไตล์ C ++ และคุณมีตัววนซ้ำสำหรับการสำรวจเส้นทางแรกที่มีความลึกเช่นนั้นfor (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }ไม่ต้องคำนึงถึงโครงสร้างของต้นไม้ที่แท้จริง (BSP, kd, Quadtree, Octree Grid เป็นต้น) การดังกล่าวจะต้อง iterator การรักษาสถานะบางอย่างเช่นparent node, child node, indexและสิ่งที่ชอบ โดยรวมท่าทางของฉันคือแม้ว่าจะมีเพียงไม่กี่ตัวอย่างเท่านั้น ...
เซบาสเตียนมัค

20

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

ในตัวอย่างต่อไปนี้โค้ดที่สร้างขึ้นจะเหมือนกันสำหรับคำนำหน้าและ postfix ตัวอย่างเช่น:

#include <stdio.h>

class Foo
{
public:

    Foo() { myData=0; }
    Foo(const Foo &rhs) { myData=rhs.myData; }

    const Foo& operator++()
    {
        this->myData++;
        return *this;
    }

    const Foo operator++(int)
    {
        Foo tmp(*this);
        this->myData++;
        return tmp;
    }

    int GetData() { return myData; }

private:

    int myData;
};

int main(int argc, char* argv[])
{
    Foo testFoo;

    int count;
    printf("Enter loop count: ");
    scanf("%d", &count);

    for(int i=0; i<count; i++)
    {
        testFoo++;
    }

    printf("Value: %d\n", testFoo.GetData());
}

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

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

ส่งผลให้ในต่อไปนี้:

00401000  push        0Ah  
00401002  push        offset string "Value: %d\n" (402104h) 
00401007  call        dword ptr [__imp__printf (4020A0h)] 

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


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

14

Google c ++ คู่มือสไตล์พูดว่า:

รูปแบบและการคาดการณ์ล่วงหน้า

ใช้แบบฟอร์มคำนำหน้า (++ i) ของตัวดำเนินการเพิ่มและลดที่มีตัววนซ้ำและวัตถุแม่แบบอื่น ๆ

คำจำกัดความ:เมื่อตัวแปรถูกเพิ่มค่า (++ i หรือ i ++) หรือลดค่า (--i หรือ i--) และไม่ได้ใช้ค่าของนิพจน์ค่าหนึ่งจะต้องตัดสินใจว่าจะเติมเงินล่วงหน้า (ลดค่า) หรือลดจำนวนลง (ลดลง)

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

ข้อด้อย:ประเพณีที่พัฒนาขึ้นใน C ใช้การเพิ่มภายหลังเมื่อไม่ใช้ค่านิพจน์โดยเฉพาะในลูป บางคนพบว่าโพสต์เพิ่มขึ้นง่ายต่อการอ่านเนื่องจาก "หัวเรื่อง" (i) นำหน้า "verb" (++) เหมือนภาษาอังกฤษ

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


1
"การตัดสินใจ: สำหรับค่าสเกลาร์แบบง่าย (ไม่ใช่วัตถุ) ไม่มีเหตุผลที่จะชอบรูปแบบเดียวและเราอนุญาตเช่นกันสำหรับตัววนซ้ำและประเภทเทมเพลตอื่นให้ใช้การเพิ่มล่วงหน้า"
Nosredna

2
เอ๊ะ, ... และนั่นคืออะไร?
Sebastian Mach

ลิงก์ที่กล่าวถึงในคำตอบนั้นใช้งานไม่ได้ในขณะนี้
karol

4

ฉันต้องการจะชี้ให้เห็นโพสต์ที่ยอดเยี่ยมโดย Andrew Koenig ใน Code Talk เมื่อเร็ว ๆ นี้

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

ที่ บริษัท ของเราเรายังใช้การประชุมของ ++ iter สำหรับความสอดคล้องและประสิทธิภาพการทำงานที่เกี่ยวข้อง แต่แอนดรูว์ให้รายละเอียดที่เกินจริงเกี่ยวกับเจตนาและประสิทธิภาพ มีบางครั้งที่เราต้องการใช้ iter ++ แทน ++ iter

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



4

@Ketan

... เพิ่มรายละเอียดที่เกินจริงเกี่ยวกับเจตนาเทียบกับประสิทธิภาพ มีบางครั้งที่เราต้องการใช้ iter ++ แทน ++ iter

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

เพราะนั่นคือเหตุผลที่ภาษาไม่ได้ถูกเรียกว่า " ++C" [*]

[*] แทรกการอภิปรายแบบบังคับเกี่ยวกับ++Cการเป็นชื่อที่มีเหตุผลมากกว่า


4
@Motti: (ล้อเล่น) ชื่อ C ++ เป็นตรรกะถ้าคุณจำ Bjarne Stroustrup C ++ เริ่มแรกเขียนมันเป็น pre-compiler สร้างโปรแกรม C ดังนั้น C ++ จึงส่งคืนค่า C เก่า หรืออาจเป็นการเสริมว่า C ++ ค่อนข้างมีข้อบกพร่องทางแนวคิดตั้งแต่เริ่มต้น
kriss

4
  1. ++ i - เร็วกว่าไม่ใช้ค่าส่งคืน
  2. i ++ - เร็วกว่าโดยใช้ค่าส่งคืน

เมื่อไม่ได้ใช้คืนค่าคอมไพเลอร์มีการประกันจะไม่ใช้ชั่วคราวในกรณีของผม++ ไม่รับประกันว่าจะเร็วกว่า แต่รับประกันว่าจะไม่ช้า

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


3

Mark: แค่ต้องการชี้ให้เห็นว่าโอเปอเรเตอร์ ++ นั้นเป็นตัวเลือกที่ดีที่จะต้องถูกแทรกและถ้าคอมไพเลอร์เลือกที่จะทำเช่นนั้นสำเนาที่ซ้ำซ้อนจะถูกกำจัดในกรณีส่วนใหญ่ (เช่นประเภท POD ซึ่งโดยทั่วไปจะเป็นตัววนซ้ำ)

ที่กล่าวว่ายังคงมีสไตล์ที่ดีกว่าในการใช้ ++ iter ในกรณีส่วนใหญ่ :-)


3

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

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

int& int::operator++() { 
     return *this += 1;
}

แต่ไม่สามารถพูดi++ได้เหมือนกัน

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

int int::operator++(int& _Val) {
    int _Original = _Val;
    _Val += 1;
    return _Original;
}

เมื่อไม่มีการทำงานที่แตกต่างระหว่าง pre-increment และ post-increment คอมไพเลอร์สามารถดำเนินการปรับให้เหมาะสมซึ่งจะไม่มีความแตกต่างระหว่างประสิทธิภาพทั้งสอง อย่างไรก็ตามหากชนิดข้อมูลคอมโพสิตเช่น a structหรือclassมีส่วนเกี่ยวข้องตัวสร้างการคัดลอกจะถูกเรียกใช้ในการเพิ่มภายหลังและจะไม่สามารถทำการปรับให้เหมาะสมนี้หากจำเป็นต้องมีการทำสำเนาลึก โดยทั่วไปการเพิ่มล่วงหน้าจะเร็วกว่าและต้องการหน่วยความจำน้อยกว่าการเพิ่มภายหลัง


1

@ Mark: ฉันลบคำตอบก่อนหน้าของฉันเพราะมันค่อนข้างพลิกและสมควรได้รับ downvote สำหรับคนเดียว จริง ๆ แล้วฉันคิดว่ามันเป็นคำถามที่ดีในแง่ที่ว่ามันถามสิ่งที่อยู่ในใจของผู้คนจำนวนมาก

คำตอบปกติคือ ++ ฉันเร็วกว่า i ++ และไม่ต้องสงสัยเลยว่ามี แต่คำถามที่ใหญ่กว่าคือ "เมื่อใดที่คุณควรสนใจ"

หากเศษส่วนของเวลา CPU ที่ใช้ในการเพิ่มตัววนซ้ำน้อยกว่า 10% แสดงว่าคุณไม่สนใจ

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

ฉันเคยเห็นตัวอย่างที่การเพิ่มตัววนซ้ำใช้เวลานานกว่า 90% ในกรณีดังกล่าวการเพิ่มจำนวนเต็มจะช่วยลดเวลาในการดำเนินการลงโดยลดจำนวนลง (เช่นดีกว่าเร่งความเร็ว 10x)


1

@wilhelmtell

คอมไพเลอร์สามารถขจัดชั่วคราว ใช้คำต่อคำจากเธรดอื่น:

คอมไพเลอร์ C ++ ได้รับอนุญาตให้กำจัดชั่วคราวตามสแต็คแม้ว่าจะทำเช่นนั้นการเปลี่ยนแปลงพฤติกรรมของโปรแกรม ลิงก์ MSDN สำหรับ VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx


1
นั่นไม่เกี่ยวข้อง NRVO หลีกเลี่ยงความจำเป็นในการคัดลอก t ใน "CC :: operator ++ (int)" กลับไปที่ผู้โทร แต่ i ++ จะยังคงคัดลอกค่าเก่าบนสแต็กของผู้โทร หากไม่มี NRVO i ++ จะสร้างสำเนา 2 ชุดสำเนาหนึ่งต่อหนึ่งและอีกหนึ่งสำเนากลับไปยังผู้โทร
Blaisorblade

0

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


3
ขออภัยที่รบกวนจิตใจฉัน ใครบอกว่าเป็น "นิสัยที่ดี" เมื่อมันแทบจะไม่มีความสำคัญอะไรเลย? หากผู้คนต้องการทำให้มันเป็นส่วนหนึ่งของวินัยของพวกเขาก็เป็นเรื่องดี แต่ขอแยกแยะเหตุผลที่สำคัญจากเรื่องของรสนิยมส่วนตัว
Mike Dunlavey

@ MikeDunlavey โอเคดังนั้นปกติแล้วคุณใช้ด้านไหนเมื่อมันไม่สำคัญ? xD เป็นอย่างใดอย่างหนึ่งไม่ได้! โพสต์ ++ (ถ้าคุณใช้ด้วยความหมายทั่วไปอัปเดตส่งคืนเก่า) ด้อยกว่า ++ โดยสมบูรณ์ก่อน (อัปเดตส่งคืน) ไม่มีเหตุผลใด ๆ ที่คุณต้องการให้ประสิทธิภาพลดลง ในกรณีที่คุณต้องการอัปเดตหลังจากนั้นโปรแกรมเมอร์จะไม่ทำแม้แต่โพสต์ ++ เลย ไม่ต้องเสียเวลาคัดลอกเมื่อเรามีมัน อัปเดตหลังจากที่เราใช้ จากนั้นคอมไพเลอร์ที่มีสามัญสำนึกคุณต้องการให้มันมี
แอ่งน้ำ

@Puddle: เมื่อฉันได้ยินสิ่งนี้: "ไม่มีเหตุผลใด ๆ ที่คุณต้องการให้มีประสิทธิภาพน้อยลง" ฉันรู้ว่าฉันได้ยิน "เพนนีฉลาด - ปอนด์โง่" คุณจำเป็นต้องรู้คุณค่าของขนาดที่เกี่ยวข้อง เฉพาะในกรณีที่บัญชีนี้มีมากกว่า 1% ของเวลาที่คุณควรคิด โดยทั่วไปหากคุณกำลังคิดเกี่ยวกับสิ่งนี้มีปัญหาใหญ่กว่าล้านเท่าที่คุณไม่ได้พิจารณาและนี่คือสิ่งที่ทำให้ซอฟต์แวร์ช้ากว่าที่ควรจะเป็น
Mike Dunlavey

@MikeDunlavey สำรอกเรื่องไร้สาระเพื่อสนองอัตตาของคุณ คุณกำลังพยายามฟังดูเหมือนนักบวชที่ฉลาด แต่คุณไม่ได้พูดอะไร ขนาดที่เกี่ยวข้อง ... ถ้าเพียง 1% ของเวลาที่คุณควรใส่ใจ ... xD เลี้ยงลูกอย่างแน่นอน ถ้ามันไม่มีประสิทธิภาพมันก็คุ้มค่าที่จะรู้และแก้ไข เรามาที่นี่เพื่อไตร่ตรองสิ่งนี้ด้วยเหตุผลที่แน่นอน! เราไม่ได้กังวลว่าเราจะได้รับความรู้นี้เท่าไหร่ และเมื่อฉันบอกว่าคุณไม่ต้องการประสิทธิภาพที่น้อยลงไปข้างหน้าอธิบายสถานการณ์เลวร้ายอย่างหนึ่ง นายฉลาด!
Puddle

0

ทั้งสองอย่างรวดเร็ว;) หากคุณต้องการมันเป็นการคำนวณแบบเดียวกันสำหรับโปรเซสเซอร์มันเป็นเพียงลำดับที่แตกต่างกัน

ตัวอย่างเช่นรหัสต่อไปนี้:

#include <stdio.h>

int main()
{
    int a = 0;
    a++;
    int b = 0;
    ++b;
    return 0;
}

ผลิตชุดประกอบต่อไปนี้:

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   $0x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   $0x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    $0x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

คุณเห็นว่าสำหรับ a ++ และ b ++ มันเป็น mnemonic รวมดังนั้นจึงเป็นการดำเนินการเดียวกัน;)


มันคือ C ในขณะที่ OP ถาม C ++ ใน C มันเหมือนกัน ใน C ++ เร็วกว่าคือ ++ i; เนื่องจากวัตถุ อย่างไรก็ตามคอมไพเลอร์บางตัวอาจปรับตัวดำเนินการหลังการเพิ่มประสิทธิภาพให้เหมาะสมที่สุด
Wiggler Jtag

0

คำถามที่ต้องการเกิดขึ้นเมื่อไม่ได้ใช้ผลลัพธ์ (ชัดเจนสำหรับคำถาม C) ใครสามารถแก้ไขได้เนื่องจากคำถามคือ "community wiki"

เกี่ยวกับการปรับให้เหมาะสมก่อนวัยอันควร Knuth มักจะอ้างถึง ถูกตัอง. แต่โดนัลด์นัทท์จะไม่ป้องกันด้วยรหัสที่น่ากลัวซึ่งคุณสามารถดูได้ในวันนี้ เคยเห็น a = b + c ท่ามกลาง Java Integers (ไม่ใช่ int) หรือไม่ นั่นคือจำนวนการแปลง 3 มวย / unboxing หลีกเลี่ยงสิ่งที่เป็นสิ่งสำคัญ และการเขียน i ++ อย่างไร้ประโยชน์แทนที่จะเป็น ++ ฉันก็เป็นความผิดพลาดเดียวกัน แก้ไข: ตามที่ phresnel ใส่ไว้อย่างดีในความคิดเห็นสิ่งนี้สามารถสรุปได้ว่า "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นสิ่งที่ชั่วร้าย

แม้ความจริงที่ว่าผู้คนคุ้นเคยกับ i ++ มากขึ้นก็เป็นมรดก C ที่โชคร้ายที่เกิดจากความผิดพลาดทางแนวคิดโดย K&R (ถ้าคุณทำตามการโต้แย้งเจตนานั่นเป็นข้อสรุปเชิงตรรกะและปกป้อง K&R เพราะพวกเขาเป็น K&R ไม่มีความหมาย ยอดเยี่ยม แต่ไม่ดีนักในการออกแบบภาษามีข้อผิดพลาดนับไม่ถ้วนในการออกแบบ C อยู่ตั้งแต่ get () ถึง strcpy () ถึง strncpy () API (ควรมี strlcpy () API ตั้งแต่วันที่ 1) )

Btw ฉันเป็นหนึ่งในผู้ที่ไม่คุ้นเคยกับ C ++ ในการค้นหา ++ ฉันน่ารำคาญที่จะอ่าน ถึงกระนั้นฉันก็ใช้มันตั้งแต่ฉันยอมรับว่ามันถูกต้อง


ฉันเห็นว่าคุณกำลังทำงานปริญญาเอก ด้วยความสนใจในการเพิ่มประสิทธิภาพของคอมไพเลอร์และสิ่งที่เรียงลำดับ เยี่ยมมาก แต่อย่าลืมสถาบันการศึกษาคือห้อง echo และสามัญสำนึกมักถูกทิ้งไว้นอกประตูอย่างน้อยใน CS คุณอาจสนใจในสิ่งนี้: stackoverflow.com/questions/1303899/ …
Mike Dunlavey

ฉันไม่เคยพบว่า++iน่ารำคาญไปกว่าi++(อันที่จริงแล้วฉันพบว่ามันเจ๋งกว่านี้) แต่ส่วนที่เหลือของโพสต์ของคุณจะได้รับการยอมรับอย่างสมบูรณ์ อาจเพิ่มจุด "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นสิ่งชั่วร้ายเช่นเดียวกับการมองในแง่ร้ายก่อนวัยอันควร"
Sebastian Mach

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

@MattMcNabb: ไม่ใช่ชื่อตัวละคร 8 ตัวที่เป็น MS-DOS แบบเอกสิทธิ์ใช่ไหม? C ถูกประดิษฐ์ด้วย Unix อย่างไรก็ตามถึงแม้ว่า strncpy มีจุดการขาด strlcpy ไม่เป็นธรรมอย่างสมบูรณ์: แม้ต้นฉบับ C มีอาร์เรย์ที่คุณไม่ควรล้นซึ่งจำเป็นต้อง strlcpy; อย่างมากพวกเขาเป็นเพียงผู้โจมตีที่ขาดหายไปจากการใช้ประโยชน์จากข้อบกพร่อง แต่ไม่มีใครบอกได้ว่าการคาดการณ์ปัญหานี้เป็นเรื่องเล็กน้อยดังนั้นหากฉันเขียนบทความใหม่ฉันจะไม่ใช้น้ำเสียงแบบเดียวกัน
Blaisorblade

@Blaisorblade: ตามที่ฉันจำได้ชื่อไฟล์ UNIX ก่อนหน้านี้มีความยาวได้ไม่เกิน 14 ตัวอักษร การขาดstrlcpy()ได้รับการพิสูจน์โดยข้อเท็จจริงว่ามันยังไม่ได้ถูกคิดค้น
Keith Thompson

-1

เวลาให้คนที่มีอัญมณีแห่งปัญญา;) - มีเคล็ดลับง่ายๆที่จะทำให้การเพิ่ม postfix ของ C ++ มีพฤติกรรมเหมือนกับการเพิ่มคำนำหน้า (ประดิษฐ์สิ่งนี้ด้วยตัวเอง แต่เห็นด้วยเช่นกันในรหัสคนอื่นดังนั้นฉันจึงไม่ อยู่คนเดียว)

โดยพื้นฐานแล้วกลอุบายคือการใช้คลาสผู้ช่วยเพื่อเลื่อนการเพิ่มขึ้นหลังจากการกลับมาและ RAII มาช่วย

#include <iostream>

class Data {
    private: class DataIncrementer {
        private: Data& _dref;

        public: DataIncrementer(Data& d) : _dref(d) {}

        public: ~DataIncrementer() {
            ++_dref;
        }
    };

    private: int _data;

    public: Data() : _data{0} {}

    public: Data(int d) : _data{d} {}

    public: Data(const Data& d) : _data{ d._data } {}

    public: Data& operator=(const Data& d) {
        _data = d._data;
        return *this;
    }

    public: ~Data() {}

    public: Data& operator++() { // prefix
        ++_data;
        return *this;
    }

    public: Data operator++(int) { // postfix
        DataIncrementer t(*this);
        return *this;
    }

    public: operator int() {
        return _data;
    }
};

int
main() {
    Data d(1);

    std::cout <<   d << '\n';
    std::cout << ++d << '\n';
    std::cout <<   d++ << '\n';
    std::cout << d << '\n';

    return 0;
}

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


-5

++iเร็วกว่าi++เพราะไม่ได้ส่งคืนค่าเดิม

นอกจากนี้ยังใช้งานง่ายยิ่งขึ้น:

x = i++;  // x contains the old value of i
y = ++i;  // y contains the new value of i 

ตัวอย่าง C นี้พิมพ์ "02" แทน "12" ที่คุณอาจคาดหวัง:

#include <stdio.h>

int main(){
    int a = 0;
    printf("%d", a++);
    printf("%d", ++a);
    return 0;
}

เหมือนกันสำหรับ C ++ :

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.