แนวทางปฏิบัติในการเข้ารหัสซึ่งช่วยให้คอมไพเลอร์ / เครื่องมือเพิ่มประสิทธิภาพสร้างโปรแกรมได้เร็วขึ้น


116

หลายปีก่อนคอมไพเลอร์ C ไม่ค่อยฉลาดนัก ในฐานะที่เป็นวิธีแก้ปัญหา K&R ได้คิดค้นคีย์เวิร์ดregisterเพื่อบอกใบ้ถึงคอมไพเลอร์ว่าอาจเป็นความคิดที่ดีที่จะเก็บตัวแปรนี้ไว้ในรีจิสเตอร์ภายใน พวกเขายังสร้างตัวดำเนินการระดับอุดมศึกษาเพื่อช่วยสร้างรหัสที่ดีขึ้น

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

FORTRAN อาจเร็วกว่า C สำหรับการดำเนินการบางประเภทเนื่องจากปัญหานามแฝง ในทางทฤษฎีด้วยการเข้ารหัสอย่างรอบคอบเราสามารถหลีกเลี่ยงข้อ จำกัด นี้เพื่อให้เครื่องมือเพิ่มประสิทธิภาพสร้างรหัสที่เร็วขึ้น

มีแนวทางปฏิบัติในการเข้ารหัสอะไรบ้างที่อาจทำให้คอมไพลเลอร์ / เครื่องมือเพิ่มประสิทธิภาพสร้างโค้ดได้เร็วขึ้น

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

นี่คือคำถามที่เกี่ยวข้อง

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

[แก้ไข] ลิงก์ที่เกี่ยวข้องกับออฟเซ็ต


7
อาจเป็นผู้สมัครที่ดีสำหรับชุมชน wiki imho เนื่องจากไม่มีคำตอบที่ชัดเจนสำหรับคำถาม (ที่น่าสนใจ) นี้ ...
ChristopheD

คิดถึงทุกครั้งเลย ขอบคุณที่ชี้ให้ดู
EvilTeach

โดย 'ดีกว่า' คุณหมายถึงเพียงแค่ 'เร็วกว่า' หรือคุณมีเกณฑ์ความเป็นเลิศอื่น ๆ ในใจ?
เครื่องหมายประสิทธิภาพสูง

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

1
@EvilTeach: วิกิชุมชนไม่ได้หมายความว่า "ไม่มีคำตอบที่ชัดเจน" แต่ไม่ตรงกันกับแท็กอัตนัย วิกิชุมชนหมายความว่าคุณต้องการมอบโพสต์ของคุณต่อชุมชนเพื่อให้คนอื่นแก้ไขได้ อย่ารู้สึกกดดันที่จะทำวิกิคำถามของคุณหากคุณไม่ต้องการ
Juliet

คำตอบ:


54

เขียนถึงตัวแปรท้องถิ่นและไม่แสดงอาร์กิวเมนต์! สิ่งนี้สามารถช่วยได้มากในการหลีกเลี่ยงการชะลอตัวของนามแฝง ตัวอย่างเช่นหากโค้ดของคุณดูเหมือน

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

คอมไพเลอร์ไม่ทราบว่า foo1! = barOut จึงต้องโหลด foo1 ใหม่ทุกครั้งผ่านลูป นอกจากนี้ยังไม่สามารถอ่าน foo2 [i] ได้จนกว่าการเขียนถึง barOut จะเสร็จสิ้น คุณสามารถเริ่มยุ่งกับพอยน์เตอร์ที่ จำกัด ได้ แต่ก็มีประสิทธิภาพ (และชัดเจนกว่ามาก) ในการทำสิ่งนี้:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

ฟังดูงี่เง่า แต่คอมไพเลอร์สามารถจัดการกับตัวแปรโลคัลได้อย่างชาญฉลาดกว่ามากเนื่องจากมันไม่สามารถซ้อนทับกันในหน่วยความจำกับอาร์กิวเมนต์ใด ๆ สิ่งนี้สามารถช่วยให้คุณหลีกเลี่ยงการโหลด Hit-store ที่น่ากลัว (กล่าวถึงโดย Francis Boivin ในหัวข้อนี้)


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

IDE ส่วนใหญ่แสดงตัวแปรในเครื่องตามค่าเริ่มต้นดังนั้นจึงมีการพิมพ์น้อยลง
EvilTeach

9
คุณยังสามารถเปิดใช้งานการเพิ่มประสิทธิภาพนั้นได้โดยใช้พอยน์เตอร์ที่ จำกัด
Ben Voigt

4
@ เบ็น - นั่นก็จริง แต่ฉันคิดว่าแบบนี้ชัดเจนกว่า นอกจากนี้หากอินพุตและเอาต์พุตทับซ้อนกันฉันเชื่อว่าผลลัพธ์นั้นไม่ได้ระบุด้วยพอยน์เตอร์ที่ จำกัด (อาจได้รับพฤติกรรมที่แตกต่างกันระหว่างดีบักและรีลีส) ในขณะที่วิธีนี้อย่างน้อยก็จะสอดคล้องกัน อย่าเข้าใจว่าฉันผิดฉันชอบใช้การ จำกัด แต่ฉันไม่ต้องการมันมากไปกว่านี้
celion

คุณต้องหวังว่า Foo จะไม่มีการดำเนินการคัดลอกที่กำหนดให้คัดลอกข้อมูลสองสามเมกะไบต์ ;-)
Skizz

76

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

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

ถัดไปโปรไฟล์รหัสของคุณ

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

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


20
นักพัฒนา Compiier มีเวลา จำกัด เช่นเดียวกับคนอื่น ๆ ไม่ใช่การปรับให้เหมาะสมทั้งหมดจะทำให้เข้าสู่คอมไพเลอร์ เช่นเดียว&กับกับ%พลังของสอง (ถ้าเคยมีการปรับให้เหมาะสม แต่อาจมีผลกระทบต่อประสิทธิภาพที่สำคัญ) หากคุณอ่านเคล็ดลับเพื่อประสิทธิภาพวิธีเดียวที่จะทราบว่าได้ผลหรือไม่คือทำการเปลี่ยนแปลงและวัดผลกระทบ อย่าคิดว่าคอมไพเลอร์จะเพิ่มประสิทธิภาพบางอย่างให้คุณ
Dave Jarvis

22
& และ% นั้นได้รับการปรับให้เหมาะสมอยู่เสมอพร้อมกับเทคนิคการคำนวณทางคณิตศาสตร์อื่น ๆ ที่ราคาถูกที่สุด สิ่งที่ไม่ได้รับการปรับให้เหมาะสมคือกรณีของตัวถูกดำเนินการทางขวามือเป็นตัวแปรที่เกิดขึ้นเพื่อเป็นกำลังสองเสมอ
Potatoswatter

8
เพื่อให้ชัดเจนฉันดูเหมือนจะสับสนผู้อ่านบางคน: คำแนะนำในการฝึกเขียนโค้ดที่ฉันเสนอคือการพัฒนาโค้ดที่ตรงไปตรงมาก่อนซึ่งไม่ได้ใช้คำแนะนำในการจัดวางหน่วยความจำเพื่อสร้างพื้นฐานของประสิทธิภาพ จากนั้นลองทำทีละอย่างและวัดผลกระทบ ฉันไม่ได้ให้คำแนะนำใด ๆ เกี่ยวกับการปฏิบัติงาน
เครื่องหมายประสิทธิภาพสูง

17
สำหรับการคงอำนาจของสองnแทนที่ GCC % nด้วยแม้ในขณะที่การเพิ่มประสิทธิภาพถูกปิดใช้งาน& (n-1) นั่นไม่ใช่ว่า "ไม่ค่อยมีถ้าเคย" ...
Porculus

12
% ไม่สามารถปรับให้เหมาะสมเป็น & เมื่อมีการลงนามประเภทเนื่องจากกฎงี่เง่าของ C สำหรับการหารจำนวนเต็มลบ (ปัดเศษเป็น 0 และมีเศษเหลือติดลบแทนที่จะปัดเศษลงและมีเศษเหลือเป็นบวกเสมอ) และส่วนใหญ่ผู้
เข้ารหัสที่

47

ลำดับที่คุณสำรวจหน่วยความจำอาจมีผลกระทบอย่างมากต่อประสิทธิภาพและคอมไพเลอร์ไม่ดีนักในการหาและแก้ไข คุณต้องคำนึงถึงความกังวลเกี่ยวกับตำแหน่งแคชเมื่อคุณเขียนโค้ดหากคุณสนใจเกี่ยวกับประสิทธิภาพ ตัวอย่างเช่นอาร์เรย์สองมิติใน C จะถูกจัดสรรในรูปแบบ row-major การข้ามอาร์เรย์ในรูปแบบหลักของคอลัมน์มีแนวโน้มที่จะทำให้คุณพลาดแคชมากขึ้นและทำให้โปรแกรมของคุณมีหน่วยความจำมากกว่าที่ตัวประมวลผลผูกไว้:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

การพูดอย่างเคร่งครัดนี่ไม่ใช่ปัญหาของเครื่องมือเพิ่มประสิทธิภาพ แต่เป็นปัญหาในการเพิ่มประสิทธิภาพ
EvilTeach

10
แน่นอนว่าเป็นปัญหาของเครื่องมือเพิ่มประสิทธิภาพ ผู้คนเขียนบทความเกี่ยวกับการเพิ่มประสิทธิภาพการแลกเปลี่ยนลูปอัตโนมัติมานานหลายทศวรรษ
Phil Miller

20
@ Potatoswatter คุยอะไรกัน? คอมไพเลอร์ C สามารถทำอะไรก็ได้ที่ต้องการตราบเท่าที่มีการสังเกตผลลัพธ์สุดท้ายเดียวกันและแน่นอนว่า GCC 4.4 มี-floop-interchangeซึ่งจะพลิกวงในและรอบนอกหากเครื่องมือเพิ่มประสิทธิภาพเห็นว่าทำกำไรได้
ephemient

2
อืมคุณไปแล้ว ความหมายของ C มักถูกทำลายโดยปัญหานามแฝง ฉันเดาว่าคำแนะนำที่แท้จริงที่นี่คือการผ่านธงนั้น!
Potatoswatter

36

การเพิ่มประสิทธิภาพทั่วไป

นี่คือการเพิ่มประสิทธิภาพที่ฉันโปรดปราน ฉันได้เพิ่มเวลาดำเนินการและลดขนาดโปรแกรมโดยใช้สิ่งเหล่านี้

ประกาศฟังก์ชันขนาดเล็กเป็นinlineหรือมาโคร

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

ลบรหัสที่ตายแล้วและซ้ำซ้อน

หากไม่ได้ใช้รหัสหรือไม่มีส่วนในผลลัพธ์ของโปรแกรมให้กำจัดออก

ลดความซับซ้อนของการออกแบบอัลกอริทึม

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

ลูป Unrolling

แต่ละลูปมีค่าใช้จ่ายในการตรวจสอบการเพิ่มและการยกเลิก ในการหาค่าประมาณของปัจจัยประสิทธิภาพให้นับจำนวนคำสั่งในค่าโสหุ้ย (ขั้นต่ำ 3: เพิ่มขึ้นตรวจสอบเริ่มต้นของลูปเริ่มต้น) แล้วหารด้วยจำนวนคำสั่งภายในลูป ตัวเลขยิ่งต่ำยิ่งดี

แก้ไข: ให้ตัวอย่างของการยกเลิกการวนซ้ำก่อนหน้านี้:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

หลังจากยกเลิกการลงทะเบียน:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

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

ฉันได้ผลลัพธ์ที่น่าทึ่งเมื่อฉันคลายการวนซ้ำเป็น 32 คำสั่ง นี่เป็นหนึ่งในปัญหาคอขวดเนื่องจากโปรแกรมต้องคำนวณการตรวจสอบในไฟล์ 2GB การเพิ่มประสิทธิภาพนี้รวมกับการอ่านบล็อกที่ปรับปรุงประสิทธิภาพจาก 1 ชั่วโมงเป็น 5 นาที การคลายการวนซ้ำยังให้ประสิทธิภาพที่ยอดเยี่ยมในภาษาแอสเซมบลีของฉันmemcpyเร็วกว่าคอมไพเลอร์memcpyมาก - TM

การลดifงบ

โปรเซสเซอร์เกลียดสาขาหรือกระโดดเนื่องจากบังคับให้โปรเซสเซอร์โหลดคิวคำสั่งซ้ำ

เลขคณิตบูลีน ( แก้ไข: ใช้รูปแบบโค้ดกับส่วนของโค้ดตัวอย่างที่เพิ่ม)

แปลงifคำสั่งเป็นการกำหนดบูลีน โปรเซสเซอร์บางตัวสามารถดำเนินการคำสั่งตามเงื่อนไขโดยไม่ต้องแยกสาขา:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

การลัดวงจรของตัวดำเนินการตรรกะ AND (&& ) ป้องกันไม่ให้การดำเนินการของการทดสอบถ้าเป็นstatusfalse

ตัวอย่าง:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

การจัดสรรตัวแปรปัจจัยภายนอกลูป

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

นิพจน์ปัจจัยคงที่ภายนอกลูป

หากการคำนวณหรือค่าตัวแปรไม่ขึ้นอยู่กับดัชนีลูปให้ย้ายออกไปนอกลูป (ก่อนหน้า)

I / O ในบล็อก

อ่านและเขียนข้อมูลเป็นกลุ่มใหญ่ (บล็อก) ใหญ่กว่าดีกว่า. ตัวอย่างเช่นการอ่านหนึ่งอ็อกเท็ตในแต่ละครั้งจะมีประสิทธิภาพน้อยกว่าการอ่าน 1024 อ็อกเท็ตด้วยการอ่านครั้งเดียว
ตัวอย่าง:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

ประสิทธิภาพของเทคนิคนี้สามารถแสดงให้เห็นได้ด้วยสายตา :-)

อย่าใช้printf ครอบครัวสำหรับข้อมูลคงที่

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

ฟอร์แมตเป็นหน่วยความจำแล้วเขียน

รูปแบบไปยังcharอาร์เรย์ใช้หลายแล้วใช้sprintf fwriteนอกจากนี้ยังช่วยให้เค้าโครงข้อมูลแบ่งออกเป็น "ส่วนคงที่" และส่วนตัวแปร คิดว่าจดหมายเวียน

ประกาศข้อความคงที่ (ตัวอักษรสตริง) เป็น static const

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

สุดท้าย Code เหมือนคอมไพเลอร์

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


2
สิ่งที่น่าสนใจคุณสามารถให้ตัวอย่างที่คุณมีโค้ดที่ดีกว่าโดยใช้คำสั่งเล็ก ๆ น้อย ๆ แทนที่จะเป็นโค้ดที่ใหญ่กว่า คุณสามารถแสดงตัวอย่างการเขียน if ใหม่โดยใช้บูลีนได้ไหม โดยทั่วไปฉันจะปล่อยให้ลูปคลายการทำงานไปยังคอมไพเลอร์เนื่องจากอาจให้ความรู้สึกที่ดีกว่าสำหรับขนาดแคช ฉันแปลกใจเล็กน้อยเกี่ยวกับแนวคิดของการวิ่งแบบ sprintfing แล้วก็เขียนด้วยลายมือ ฉันคิดว่า fprintf ทำแบบนั้นภายใต้ฝากระโปรง คุณสามารถให้รายละเอียดเพิ่มเติมที่นี่ได้ไหม
EvilTeach

1
ไม่มีการรับประกันว่าการfprintfฟอร์แมตไปยังบัฟเฟอร์แยกต่างหากจากนั้นจะส่งเอาต์พุตบัฟเฟอร์ การปรับปรุงประสิทธิภาพ (สำหรับการใช้หน่วยความจำ) fprintfจะส่งออกข้อความที่ไม่ได้จัดรูปแบบทั้งหมดจากนั้นจัดรูปแบบและเอาต์พุตและทำซ้ำจนกว่าสตริงรูปแบบทั้งหมดจะได้รับการประมวลผลจึงทำการเรียกเอาต์พุต 1 รายการสำหรับเอาต์พุตแต่ละประเภท (จัดรูปแบบกับไม่ฟอร์แมต) การใช้งานอื่น ๆ จะต้องจัดสรรหน่วยความจำแบบไดนามิกสำหรับการเรียกแต่ละครั้งเพื่อเก็บสตริงใหม่ทั้งหมด (ซึ่งไม่ดีในสภาพแวดล้อมระบบฝังตัว) ข้อเสนอแนะของฉันลดจำนวนผลลัพธ์
Thomas Matthews

3
ครั้งหนึ่งฉันได้รับการปรับปรุงประสิทธิภาพที่สำคัญโดยการหมุนวน จากนั้นฉันก็หาวิธีรวบรวมให้แน่นขึ้นโดยใช้ทิศทางบางอย่างและโปรแกรมก็เร็วขึ้นอย่างเห็นได้ชัด (การทำโปรไฟล์แสดงให้เห็นว่าฟังก์ชันเฉพาะนี้เป็น 60-80% ของรันไทม์และฉันทดสอบประสิทธิภาพอย่างรอบคอบทั้งก่อนและหลัง) ฉันเชื่อว่าการปรับปรุงเกิดจากพื้นที่ที่ดีขึ้น แต่ฉันไม่แน่ใจในเรื่องนั้นอย่างสมบูรณ์
David Thornley

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

26

เครื่องมือเพิ่มประสิทธิภาพไม่ได้ควบคุมประสิทธิภาพของโปรแกรมของคุณจริงๆ ใช้อัลกอริทึมและโครงสร้างและโปรไฟล์โปรไฟล์โปรไฟล์ที่เหมาะสม

ที่กล่าวว่าคุณไม่ควรอินเนอร์ในฟังก์ชันขนาดเล็กจากไฟล์หนึ่งในไฟล์อื่นเนื่องจากจะหยุดไม่ให้อินไลน์

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

ซึ่งนำไปสู่ประเด็นต่อไปอ่านคู่มือ ^ # $ @ ! GCC สามารถกำหนดรหัส C ธรรมดาเป็นเวกเตอร์ได้หากคุณโรย__restrict__ที่นี่และที่__attribute__( __aligned__ )นั่น หากคุณต้องการบางสิ่งที่เฉพาะเจาะจงมากจากเครื่องมือเพิ่มประสิทธิภาพคุณอาจต้องเจาะจง


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

1
@Novelocrat อ้อ - ความจำเป็นที่จะบอกว่าผมรู้สึกประหลาดใจมากครั้งแรกที่ผมเห็นอะไรบางอย่างจากA.cได้รับ inlined B.cเข้า
Jonathon Reinhart

18

สำหรับโปรเซสเซอร์ที่ทันสมัยส่วนใหญ่คอขวดที่ใหญ่ที่สุดคือหน่วยความจำ

นามแฝง: Load-Hit-Store สามารถทำลายล้างได้ในวง จำกัด หากคุณกำลังอ่านตำแหน่งหน่วยความจำหนึ่งและเขียนไปยังอีกตำแหน่งหนึ่งและรู้ว่าไม่ปะติดปะต่อกันการใส่คีย์เวิร์ดนามแฝงอย่างระมัดระวังในพารามิเตอร์ฟังก์ชันจะช่วยให้คอมไพเลอร์สร้างโค้ดได้เร็วขึ้น อย่างไรก็ตามหากพื้นที่หน่วยความจำทับซ้อนกันและคุณใช้ 'นามแฝง' แสดงว่าคุณกำลังอยู่ในช่วงการแก้ไขจุดบกพร่องของพฤติกรรมที่ไม่ได้กำหนด!

Cache-miss: ไม่แน่ใจว่าคุณสามารถช่วยคอมไพเลอร์ได้อย่างไรเนื่องจากส่วนใหญ่เป็นอัลกอริทึม แต่มีอินทรินนิกในการดึงหน่วยความจำล่วงหน้า

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


4
+1 สำหรับร้านค้าโหลด Hit และประเภทการลงทะเบียนที่แตกต่างกัน ฉันไม่แน่ใจว่าข้อตกลงนั้นใหญ่แค่ไหนใน x86 แต่พวกเขากำลังลงทุนกับ PowerPC (เช่น Xbox360 และ Playstation3)
celion

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

11

รหัสส่วนใหญ่ที่ผู้คนเขียนจะถูกผูกไว้กับ I / O (ฉันเชื่อว่ารหัสทั้งหมดที่ฉันเขียนด้วยเงินในช่วง 30 ปีที่ผ่านมานั้นผูกพันกันมาก) ดังนั้นกิจกรรมของเครื่องมือเพิ่มประสิทธิภาพสำหรับคนส่วนใหญ่จึงเป็นเรื่องวิชาการ

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


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

6
ทรู; แต่โค้ดที่ผูกกับ I / O ที่แย่มากในปัจจุบันเขียนด้วยภาษาที่ใช้ในแง่ร้าย - ภาษาที่ไม่มีแม้แต่คอมไพเลอร์ ฉันสงสัยว่าพื้นที่ที่ยังคงใช้ C และ C ++ นั้นมักจะเป็นพื้นที่ที่มีความสำคัญมากกว่าในการเพิ่มประสิทธิภาพบางอย่าง (การใช้งาน CPU การใช้หน่วยความจำขนาดรหัส ... )
Porculus

3
ฉันใช้เวลาส่วนใหญ่ในช่วง 30 ปีที่ผ่านมาในการทำงานกับโค้ดโดยมี I / O น้อยมาก บันทึกเป็นเวลา 2 ปีในการทำฐานข้อมูล กราฟิกระบบควบคุมการจำลอง - ไม่มีการผูก I / O ใด ๆ หาก I / O เป็นปัญหาคอขวดของคนส่วนใหญ่เราจะไม่ให้ความสนใจกับ Intel และ AMD มากนัก
phkahler

2
ใช่ฉันไม่ได้ซื้อข้อโต้แย้งนี้จริงๆมิฉะนั้นเรา (ที่ทำงานของฉัน) จะไม่มองหาวิธีที่จะใช้เวลาในการคำนวณมากขึ้นในการทำ I / O นอกจากนี้ - ซอฟต์แวร์ผูก I / O ส่วนใหญ่ที่ฉันเจอนั้นถูกผูกไว้กับ I / O เพราะ I / O นั้นทำอย่างเละเทะ หากมีการปรับรูปแบบการเข้าถึงให้เหมาะสม (เช่นเดียวกับหน่วยความจำ) เราจะได้รับประสิทธิภาพที่เพิ่มขึ้นอย่างมาก
ประ - เถิดเทิง

3
ฉันเพิ่งค้นพบว่าแทบไม่มีโค้ดที่เขียนในภาษา C ++ ที่ผูก I / O แน่นอนว่าหากคุณกำลังเรียกใช้ฟังก์ชัน OS สำหรับการถ่ายโอนดิสก์จำนวนมากเธรดของคุณอาจเข้าสู่การรอ I / O (แต่ด้วยการแคชแม้ว่าจะเป็นเรื่องที่น่าสงสัยก็ตาม) แต่ฟังก์ชั่นไลบรารี I / O ตามปกติสิ่งที่ทุกคนแนะนำเนื่องจากเป็นแบบมาตรฐานและแบบพกพานั้นช้าอย่างน่าอนาถเมื่อเทียบกับเทคโนโลยีดิสก์สมัยใหม่ (แม้แต่ของที่มีราคาปานกลาง) เป็นไปได้มากว่า I / O เป็นคอขวดก็ต่อเมื่อคุณล้างข้อมูลไปยังดิสก์ทั้งหมดหลังจากเขียนเพียงไม่กี่ไบต์ OTOH UI เป็นคนละเรื่องมนุษย์เราช้า
Ben Voigt

11

ใช้ความถูกต้อง const ให้มากที่สุดในโค้ดของคุณ ช่วยให้คอมไพเลอร์ปรับแต่งได้ดีขึ้นมาก

ในเอกสารนี้ประกอบด้วยเคล็ดลับการเพิ่มประสิทธิภาพอื่น ๆ มากมาย: การเพิ่มประสิทธิภาพ CPP (แม้ว่าเอกสารเก่าไปหน่อย)

ไฮไลท์:

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

8
ไม่มากไม่ค่อยมี แม้ว่าจะปรับปรุงความถูกต้องตามความเป็นจริง
Potatoswatter

5
ใน C และ C ++ คอมไพลเลอร์ไม่สามารถใช้ const เพื่อปรับให้เหมาะสมได้เนื่องจากการแคสต์ออกไปเป็นพฤติกรรมที่กำหนดไว้อย่างดี
dsimcha

+1: const เป็นตัวอย่างที่ดีของสิ่งที่จะส่งผลโดยตรงต่อโค้ดที่คอมไพล์ ความคิดเห็นของ re @ dsimcha - คอมไพเลอร์ที่ดีจะทดสอบเพื่อดูว่าสิ่งนี้เกิดขึ้นหรือไม่ แน่นอนว่าคอมไพเลอร์ที่ดีจะ "หา" องค์ประกอบ const ที่ไม่ได้ประกาศแบบนั้นอยู่ดี ...
Hogan

@dsimcha: อย่างไรก็ตามการเปลี่ยนตัวชี้const และ restrictตัวชี้ที่มีคุณสมบัติเหมาะสมนั้นไม่ได้กำหนดไว้ ดังนั้นคอมไพเลอร์สามารถปรับให้เหมาะสมแตกต่างกันในกรณีเช่นนี้
Dietrich Epp

6
@dsimcha หล่อออกไปconstในconstการอ้างอิงหรือconstชี้ไปยังที่ไม่ใช่constวัตถุที่ดีที่กำหนด การปรับเปลี่ยนconstออบเจ็กต์จริง(เช่นที่ประกาศไว้constเดิม) ไม่ได้
Stephen Lin

9

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

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

ใช้ประโยชน์จากการเปรียบเทียบกับ 0 ฟรีที่โปรเซสเซอร์ส่วนใหญ่มอบให้คุณเมื่อทำการคำนวณทางคณิตศาสตร์หรือตรรกะ คุณมักจะได้รับค่าสถานะสำหรับ == 0 และ <0 ซึ่งคุณสามารถรับ 3 เงื่อนไขได้อย่างง่ายดาย:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

มักจะถูกกว่าการทดสอบค่าคงที่อื่น ๆ

เคล็ดลับอีกประการหนึ่งคือการใช้การลบเพื่อกำจัดการเปรียบเทียบในการทดสอบช่วง

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

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

เมื่อใช้ฟังก์ชันสตริงใน c (strcpy, memcpy, ... ) จำสิ่งที่ส่งคืน - ปลายทาง! คุณมักจะได้รับรหัสที่ดีกว่าโดยการ 'ลืม' สำเนาตัวชี้ไปยังปลายทางและเพียงแค่ดึงกลับมาจากการส่งคืนฟังก์ชันเหล่านี้

อย่ามองข้ามโอกาสที่จะส่งคืนสิ่งเดียวกันกับฟังก์ชันสุดท้ายที่คุณเรียกกลับมา คอมไพเลอร์ไม่ค่อยเก่งในการเลือกสิ่งนั้น:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

แน่นอนคุณสามารถย้อนกลับตรรกะที่ว่าถ้ามีจุดกลับเพียงจุดเดียว

(เทคนิคที่ฉันจำได้ในภายหลัง)

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


2
ที่จริงแล้วไม่จำเป็นต้องใช้การลบเมื่อทำการทดสอบช่วง LLVM, GCC และคอมไพเลอร์ของฉันอย่างน้อยก็ทำโดยอัตโนมัติ มีเพียงไม่กี่คนที่อาจเข้าใจว่าโค้ดที่มีการลบทำอะไรได้บ้างและยังมีน้อยลงด้วยเหตุใดจึงใช้งานได้จริง
Gratian Lup

ในตัวอย่างด้านบนไม่สามารถเรียก b () ได้เพราะถ้า (x <0) แล้ว a () จะถูกเรียก
EvilTeach

@EvilTeach ไม่มันจะไม่ การเปรียบเทียบที่ส่งผลให้มีการเรียก a () คือ! x
nategoose

@nategoose ถ้า x เป็น -3 แล้ว! x เป็นจริง
EvilTeach

@EvilTeach ใน C 0 เป็นเท็จและทุกอย่างเป็นจริงดังนั้น -3 จึงเป็นจริงดังนั้น -3 จึงเป็นเท็จ
nategoose

9

ฉันเขียนคอมไพเลอร์ C ที่ปรับให้เหมาะสมแล้วและนี่คือสิ่งที่มีประโยชน์มากที่ควรพิจารณา:

  1. ทำให้ฟังก์ชันส่วนใหญ่คงที่ สิ่งนี้ช่วยให้การแพร่กระจายค่าคงที่ระหว่างโพรซีเดอร์และการวิเคราะห์นามแฝงทำงานได้มิฉะนั้นคอมไพเลอร์จำเป็นต้องสันนิษฐานว่าฟังก์ชันสามารถเรียกใช้จากภายนอกหน่วยการแปลโดยมีค่าที่ไม่รู้จักโดยสิ้นเชิงสำหรับพารามิเตอร์ หากคุณดูที่ไลบรารีโอเพนซอร์สที่รู้จักกันดีพวกเขาทั้งหมดจะทำเครื่องหมายฟังก์ชันคงที่ยกเว้นที่จำเป็นต้องอยู่ภายนอกจริงๆ

  2. หากใช้ตัวแปรส่วนกลางให้ทำเครื่องหมายคงที่และค่าคงที่ถ้าเป็นไปได้ หากมีการเตรียมใช้งานครั้งเดียว (อ่านอย่างเดียว) ควรใช้รายการเริ่มต้นเช่น static const int VAL [] = {1,2,3,4} ไม่เช่นนั้นคอมไพเลอร์อาจไม่พบว่าตัวแปรเป็นค่าเริ่มต้นจริงและ จะไม่สามารถแทนที่โหลดจากตัวแปรด้วยค่าคงที่

  3. ไม่เคยใช้ goto ที่ด้านในของลูปคอมไพเลอร์ส่วนใหญ่จะไม่รู้จักลูปอีกต่อไปและจะไม่มีการปรับใช้การปรับให้เหมาะสมที่สำคัญที่สุด

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

  5. ใช้อาร์เรย์แทนตัวชี้ทุกครั้งที่ทำได้โดยเฉพาะในลูป (a [i]) โดยปกติอาร์เรย์จะให้ข้อมูลเพิ่มเติมสำหรับการวิเคราะห์นามแฝงและหลังจากการเพิ่มประสิทธิภาพบางอย่างโค้ดเดียวกันจะถูกสร้างขึ้นต่อไป (ค้นหาการลดความแรงของลูปหากสงสัย) นอกจากนี้ยังเพิ่มโอกาสในการใช้การเคลื่อนที่ของโค้ดแบบวนซ้ำ

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

  7. เมื่อเขียนแบบทดสอบที่มีเงื่อนไขหลายข้อให้เลือกแบบทดสอบที่มีโอกาสมากที่สุดก่อน if (a || b || c) ควรเป็น if (b || a || c) ถ้าbมีแนวโน้มที่จะเป็นจริงมากกว่าตัวอื่น ๆ โดยทั่วไปแล้วคอมไพเลอร์จะไม่รู้อะไรเลยเกี่ยวกับค่าที่เป็นไปได้ของเงื่อนไขและสาขาใดที่ถูกนำมาใช้มากกว่า (สามารถทราบได้โดยใช้ข้อมูลโปรไฟล์ แต่มีโปรแกรมเมอร์เพียงไม่กี่คนที่ใช้)

  8. การใช้สวิตช์เร็วกว่าการทดสอบเช่น if (a || b || ... || z) ตรวจสอบก่อนว่าคอมไพเลอร์ของคุณไม่นี้โดยอัตโนมัติบางคนทำและมันอ่านได้มากขึ้นที่จะมีถ้าแม้ว่า


7

ในกรณีของระบบฝังตัวและโค้ดที่เขียนด้วย C / C ++ ฉันพยายามหลีกเลี่ยงการจัดสรรหน่วยความจำแบบไดนามิกมากที่สุด เหตุผลหลักที่ฉันทำเช่นนี้ไม่จำเป็นต้องมีประสิทธิภาพ แต่กฎทั่วไปนี้มีผลกระทบด้านประสิทธิภาพ

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


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

7

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

ส่งผ่านอาร์กิวเมนต์ของฟังก์ชันตามลำดับเดียวกันเสมอ

หากคุณมี f_1 (x, y, z) ซึ่งเรียกใช้ f_2 ให้ประกาศ f_2 เป็น f_2 (x, y, z) อย่าประกาศว่าเป็น f_2 (x, z, y)

เหตุผลก็คือแพลตฟอร์ม C / C ++ ABI (AKA Calling Convention) สัญญาว่าจะส่งผ่านข้อโต้แย้งในการลงทะเบียนและตำแหน่งสแต็กโดยเฉพาะ เมื่ออาร์กิวเมนต์อยู่ในรีจิสเตอร์ที่ถูกต้องแล้วก็ไม่จำเป็นต้องย้ายไปมา

ในขณะที่อ่านรหัสแยกชิ้นส่วนฉันเคยเห็นการสับเปลี่ยนทะเบียนที่ไร้สาระเพราะคนไม่ปฏิบัติตามกฎนี้


2
ทั้ง C และ C ++ ไม่รับประกันใด ๆ เกี่ยวกับหรือแม้แต่กล่าวถึงการส่งผ่านการลงทะเบียนหรือตำแหน่งสแต็กโดยเฉพาะ เป็นABI (เช่น Linux ELF) ที่กำหนดรายละเอียดการส่งผ่านพารามิเตอร์
Emmet

5

เทคนิคการเข้ารหัสสองรายการที่ฉันไม่เห็นในรายการด้านบน:

บายพาสตัวเชื่อมโยงโดยการเขียนโค้ดเป็นแหล่งเฉพาะ

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

แต่ถ้าคุณออกแบบโปรแกรมได้ดีคุณสามารถรวบรวมผ่านแหล่งข้อมูลทั่วไปที่ไม่ซ้ำใครได้ นั่นคือแทนที่จะคอมไพล์ unit1.c และ unit2.c จากนั้นลิงก์อ็อบเจ็กต์ทั้งสองคอมไพล์ all.c ที่เป็นเพียง #include unit1.c และ unit2.c ดังนั้นคุณจะได้รับประโยชน์จากการปรับแต่งคอมไพเลอร์ทั้งหมด

มันเหมือนกับการเขียนส่วนหัวเฉพาะโปรแกรมใน C ++ (และทำได้ง่ายกว่าใน C)

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

การใช้เทคนิคง่ายๆนี้ทำให้ฉันเขียนโปรแกรมได้เร็วขึ้นสิบเท่า!

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

แยกงานปรมาณูในลูป

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

โปรดใช้ความระมัดระวังกับสิ่งนี้ (การแสดงโปรไฟล์โดยใช้เคล็ดลับนี้หรือไม่) เช่นเดียวกับการใช้ register อาจให้ประสิทธิภาพน้อยกว่าการปรับปรุง


2
ใช่ตอนนี้ LTO ทำให้ครึ่งแรกของโพสต์นี้ซ้ำซ้อนและอาจเป็นคำแนะนำที่ไม่ดี
underscore_d

@underscore_d: ยังคงมีปัญหาบางอย่าง (ส่วนใหญ่เกี่ยวข้องกับการมองเห็นสัญลักษณ์ที่ส่งออก) แต่จากมุมมองด้านประสิทธิภาพเพียงอย่างเดียวอาจไม่มีอะไรเพิ่มเติม
kriss

4

ฉันเคยเห็นสิ่งนี้เสร็จแล้วใน SQLite และพวกเขาอ้างว่ามันส่งผลให้ประสิทธิภาพเพิ่มขึ้น ~ 5%: ใส่รหัสทั้งหมดของคุณในไฟล์เดียวหรือใช้ตัวประมวลผลล่วงหน้าเพื่อทำสิ่งนี้ให้เทียบเท่า วิธีนี้เครื่องมือเพิ่มประสิทธิภาพจะสามารถเข้าถึงโปรแกรมทั้งหมดและสามารถทำการเพิ่มประสิทธิภาพระหว่างกระบวนการได้มากขึ้น


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

คอมไพลเลอร์ AIX มีสวิตช์คอมไพเลอร์เพื่อกระตุ้นให้เกิดพฤติกรรมนั้น -qipa [= <suboptions_list>] | -qnoipa เปิดหรือปรับแต่งคลาสของการเพิ่มประสิทธิภาพที่เรียกว่าการวิเคราะห์ระหว่างกระบวนการ (IPA)
EvilTeach

4
ดีที่สุดคือมีวิธีการพัฒนาที่ไม่ต้องใช้สิ่งนี้ การใช้ข้อเท็จจริงนี้เป็นข้ออ้างในการเขียนโค้ด un-modular โดยรวมจะส่งผลให้โค้ดทำงานช้าและมีปัญหาในการบำรุงรักษา
Hogan

3
ฉันคิดว่าข้อมูลนี้ล้าสมัยไปเล็กน้อย ในทางทฤษฎีคุณสมบัติการเพิ่มประสิทธิภาพโปรแกรมทั้งหมดที่มีอยู่ในคอมไพเลอร์จำนวนมากในขณะนี้ (เช่น "Link-time Optimization" ใน gcc) อนุญาตให้ใช้ประโยชน์ได้เหมือนกัน แต่ด้วยเวิร์กโฟลว์มาตรฐานโดยสิ้นเชิง (บวกเวลาการคอมไพล์ใหม่ที่เร็วกว่าการรวมทั้งหมดไว้ในไฟล์เดียว !)
Ponkadoodle

@Wallacoloo แน่นอนว่านี่คือวันที่ออกนอกบ้าน FWIW ฉันเพิ่งใช้ LTO ของ GCC เป็นครั้งแรกในวันนี้และสิ่งอื่น ๆ ที่เท่ากัน-O3- มันทำลาย 22% ของขนาดดั้งเดิมจากโปรแกรมของฉัน (มันไม่ได้เชื่อมต่อกับ CPU ดังนั้นฉันจึงไม่ได้พูดถึงความเร็วมากนัก)
underscore_d

4

คอมไพเลอร์สมัยใหม่ส่วนใหญ่ควรทำงานได้ดีในการเร่งการเรียกใช้หางซ้ำเนื่องจากการเรียกฟังก์ชันสามารถปรับให้เหมาะสมได้

ตัวอย่าง:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

แน่นอนว่าตัวอย่างนี้ไม่มีการตรวจสอบขอบเขตใด ๆ

แก้ไขล่าช้า

ในขณะที่ฉันไม่มีความรู้โดยตรงเกี่ยวกับรหัส ดูเหมือนชัดเจนว่าความต้องการในการใช้ CTE บน SQL Server ได้รับการออกแบบมาโดยเฉพาะเพื่อให้สามารถปรับให้เหมาะสมผ่านการเรียกซ้ำแบบ end-end


1
คำถามเกี่ยวกับ C. C ไม่ลบการวนซ้ำหางดังนั้นหางหรือการเรียกซ้ำอื่น ๆ สแต็กอาจระเบิดหากการเรียกซ้ำลึกเกินไป
คางคก

1
ฉันได้หลีกเลี่ยงปัญหาการเรียกประชุมโดยใช้ goto มีค่าใช้จ่ายน้อยกว่าด้วยวิธีนั้น
EvilTeach

2
@hogan: นี่เป็นเรื่องใหม่สำหรับฉัน คุณช่วยชี้ไปที่คอมไพเลอร์ที่ทำสิ่งนี้ได้ไหม และคุณจะแน่ใจได้อย่างไรว่ามันเพิ่มประสิทธิภาพได้จริง? หากจะทำสิ่งนี้จริงๆต้องแน่ใจว่าทำได้ ไม่ใช่สิ่งที่คุณหวังว่าเครื่องมือเพิ่มประสิทธิภาพคอมไพเลอร์จะหยิบขึ้นมา (เช่นอินไลน์ซึ่งอาจใช้งานได้หรือไม่ได้)
คางคก

6
@hogan: ฉันยืนหยัดแก้ไข คุณคิดถูกแล้วที่ Gcc และ MSVC ทั้งคู่ทำการเพิ่มประสิทธิภาพการเรียกซ้ำหาง
คางคก

5
ตัวอย่างนี้ไม่ใช่การเรียกซ้ำหางเนื่องจากไม่ใช่การเรียกซ้ำที่เป็นครั้งสุดท้าย แต่เป็นการคูณ
Brian Young

4

อย่าทำงานเดิมซ้ำแล้วซ้ำเล่า!

ปฏิปักษ์ทั่วไปที่ฉันเห็นไปตามบรรทัดเหล่านี้:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

คอมไพเลอร์ต้องเรียกใช้ฟังก์ชันเหล่านั้นทั้งหมดตลอดเวลา สมมติว่าคุณเป็นโปรแกรมเมอร์รู้ว่าวัตถุที่รวมกันนั้นไม่ได้เปลี่ยนแปลงไปตลอดการเรียกร้องเหล่านี้เพราะความรักของทุกสิ่งที่ศักดิ์สิทธิ์ ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

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


3
  1. ใช้ขอบเขตโลคัลมากที่สุดสำหรับการประกาศตัวแปรทั้งหมด

  2. ใช้constทุกครั้งที่ทำได้

  3. อย่าใช้การลงทะเบียนเว้นแต่คุณจะวางแผนที่จะโปรไฟล์ทั้งที่มีและไม่มีมัน

2 รายการแรกโดยเฉพาะ # 1 หนึ่งช่วยให้เครื่องมือเพิ่มประสิทธิภาพวิเคราะห์โค้ด โดยเฉพาะอย่างยิ่งจะช่วยในการตัดสินใจที่ดีเกี่ยวกับตัวแปรที่จะเก็บไว้ในการลงทะเบียน

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

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



3

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

ปัญหาในกรณีนี้คือซอฟต์แวร์ที่เราใช้มีการจัดสรรเพียงเล็กน้อยมากมาย เช่นเดียวกับการจัดสรรสี่ไบต์ตรงนี้หกไบต์ที่นั่น ฯลฯ มีวัตถุเล็ก ๆ จำนวนมากเช่นกันที่ทำงานในช่วง 8-12 ไบต์ ปัญหาไม่ได้มากนักที่โปรแกรมต้องการสิ่งเล็ก ๆ น้อย ๆ มากมายมันเป็นการจัดสรรสิ่งเล็ก ๆ น้อย ๆ จำนวนมากทีละรายการซึ่งทำให้การจัดสรรแต่ละครั้งขยายออกไป (บนแพลตฟอร์มเฉพาะนี้) 32 ไบต์

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

ส่วนอื่น ๆ ของการแก้ปัญหาคือการแทนที่การใช้สมาชิก char * ที่จัดการด้วยตนเองด้วยสตริง SSO (small-string optimization) การจัดสรรขั้นต่ำคือ 32 ไบต์ฉันสร้างคลาสสตริงที่มีบัฟเฟอร์ 28 อักขระฝังอยู่ด้านหลัง char * ดังนั้น 95% ของสตริงของเราจึงไม่จำเป็นต้องทำการจัดสรรเพิ่มเติม (จากนั้นฉันก็แทนที่ด้วยตนเองเกือบทุกลักษณะของ ถ่าน * ในไลบรารีนี้ด้วยคลาสใหม่นี้สนุกหรือไม่) สิ่งนี้ช่วยคนจำนวนมากด้วยการกระจายตัวของหน่วยความจำเช่นกันซึ่งจะเพิ่มตำแหน่งในการอ้างอิงสำหรับวัตถุที่ชี้ไปยังอื่น ๆ และในทำนองเดียวกันก็มีการเพิ่มประสิทธิภาพ


3

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

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

2

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

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


การแปลงการเรียกซ้ำเป็นสแต็กเป็นการเพิ่มประสิทธิภาพที่สมมติขึ้นบน ompf.org สำหรับผู้ที่พัฒนา raytracers และเขียนอัลกอริทึมการเรนเดอร์อื่น ๆ
ทอม

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

2

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

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

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

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

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

ความนับถือ

เครื่องหมาย

เมื่อฉันหยิบโค้ดขึ้นมาเป็นครั้งแรกฉันมักจะได้รับประสิทธิภาพที่เพิ่มขึ้น 1.4 - 2.0 เท่า (เช่นโค้ดเวอร์ชันใหม่จะทำงานใน 1 / 1.4 หรือ 1/2 ของเวลาของเวอร์ชันเก่า) ภายในระยะเวลา วันหรือสองวันโดยการเล่นซอกับแฟล็กคอมไพเลอร์ จริงอยู่นั่นอาจเป็นความคิดเห็นเกี่ยวกับการขาดความเข้าใจในคอมไพเลอร์ในหมู่นักวิทยาศาสตร์ที่สร้างโค้ดส่วนใหญ่ที่ฉันทำงานอยู่แทนที่จะเป็นอาการของความเป็นเลิศของฉัน การตั้งค่าแฟล็กคอมไพเลอร์เป็นสูงสุด (และไม่ค่อยมีแค่ -O3) อาจใช้เวลาหลายเดือนในการทำงานอย่างหนักเพื่อให้ได้ปัจจัยอื่นเป็น 1.05 หรือ 1.1


2

เมื่อ DEC ออกมาพร้อมกับตัวประมวลผลอัลฟ่ามีคำแนะนำให้คงจำนวนอาร์กิวเมนต์ไว้ในฟังก์ชันให้ต่ำกว่า 7 เนื่องจากคอมไพเลอร์จะพยายามใส่อาร์กิวเมนต์สูงสุด 6 ตัวในการลงทะเบียนโดยอัตโนมัติ


x86-64 บิตยังอนุญาตให้มีพารามิเตอร์การลงทะเบียนจำนวนมากซึ่งอาจมีผลอย่างมากต่อค่าใช้จ่ายในการเรียกฟังก์ชัน
ทอม

1

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

เครื่องมือเพิ่มประสิทธิภาพจะช่วยให้โปรแกรมของคุณมีประสิทธิภาพเพียงเล็กน้อย


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

1

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

สมมติว่าโปรแกรมได้รับการเขียนอย่างถูกต้องรวบรวมด้วยการเพิ่มประสิทธิภาพเต็มรูปแบบทดสอบและนำไปใช้ในการผลิต

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

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

หลังจากเสร็จสิ้นการเพิ่มประสิทธิภาพระดับไมโคร (ของฮอตสปอต) สามารถให้ผลตอบแทนที่ดีแก่คุณ


1

ฉันใช้คอมไพเลอร์ intel ทั้งบน Windows และ Linux

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

หากโค้ดเป็นรหัสคำนวณและมีลูปจำนวนมาก - รายงาน vectorization ในคอมไพเลอร์ intel จะมีประโยชน์มากให้มองหา 'vec-report' ในความช่วยเหลือ

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


คุณใกล้จะตอบคำถามแล้ว ..... คุณทำอะไรกับโค้ดเพื่อให้คอมไพลเลอร์ทำการเพิ่มประสิทธิภาพประเภทนั้นได้?
EvilTeach

1
พยายามเขียนให้มากขึ้นในรูปแบบ C (เทียบกับ C ++) เช่นหลีกเลี่ยงฟังก์ชันเสมือนจริงที่ไม่มีความจำเป็นอย่างยิ่งโดยเฉพาะอย่างยิ่งถ้าพวกเขาจะถูกเรียกบ่อยๆให้หลีกเลี่ยง AddRefs .. และสิ่งดีๆทั้งหมด (อีกครั้งเว้นแต่จะจำเป็นจริงๆ) เขียนโค้ดได้ง่ายสำหรับการอินไลน์ - พารามิเตอร์น้อยลงน้อยกว่า "if" -s ไม่ใช้ตัวแปรส่วนกลางเว้นแต่จำเป็นอย่างยิ่ง ในโครงสร้างข้อมูล - ใส่ฟิลด์ที่กว้างขึ้นก่อน (สองครั้ง, int64 ไปก่อน int) - ดังนั้นคอมไพเลอร์จึงจัดโครงสร้างตามขนาดธรรมชาติของฟิลด์แรก - จัดแนวที่ดีสำหรับความสมบูรณ์แบบ
.

1
การจัดวางและการเข้าถึงข้อมูลมีความสำคัญอย่างยิ่งต่อประสิทธิภาพ ดังนั้นหลังจากการสร้างโปรไฟล์ - บางครั้งฉันก็แบ่งโครงสร้างออกเป็นหลาย ๆ อันตามพื้นที่ของการเข้าถึง อีกหนึ่งเคล็ดลับทั่วไป - ใช้ int หรือ size-t เทียบกับ char แม้ค่าข้อมูลจะน้อยก็ตาม - หลีกเลี่ยงความสมบูรณ์แบบต่างๆ เก็บบทลงโทษเพื่อโหลดการบล็อกปัญหาเกี่ยวกับแผงลอยลงทะเบียนบางส่วน แน่นอนว่าสิ่งนี้ใช้ไม่ได้เมื่อต้องการอาร์เรย์ขนาดใหญ่ของข้อมูลดังกล่าว
.

อีกอย่างหนึ่ง - หลีกเลี่ยงการโทรระบบเว้นแต่ว่ามีความจำเป็นจริง ๆ :) - มีราคาแพงมาก
jf.

2
@jf: ฉัน +1 คำตอบของคุณ แต่ช่วยย้ายคำตอบจากความคิดเห็นไปเป็นเนื้อหาคำตอบได้ไหม มันจะอ่านง่ายขึ้น
kriss

1

การเพิ่มประสิทธิภาพอย่างหนึ่งที่ฉันใช้ใน C ++ คือการสร้างตัวสร้างที่ไม่ทำอะไรเลย เราต้องเรียกใช้ init () ด้วยตนเองเพื่อให้วัตถุอยู่ในสถานะใช้งานได้

สิ่งนี้มีประโยชน์ในกรณีที่ฉันต้องการเวกเตอร์ขนาดใหญ่ของคลาสเหล่านี้

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

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


6
ฉันเชื่อว่าการใช้งานทั่วไปของ std :: vector ไม่ได้สร้างวัตถุเพิ่มเติมเมื่อคุณจอง () ความจุมากขึ้น มันแค่จัดสรรหน้า ตัวสร้างจะถูกเรียกในภายหลังโดยใช้ตำแหน่งใหม่เมื่อคุณเพิ่มวัตถุลงในเวกเตอร์ - ซึ่ง (น่าจะเป็น) ก่อนที่คุณจะเรียก init () ดังนั้นคุณจึงไม่จำเป็นต้องใช้ฟังก์ชัน init () แยก โปรดจำไว้ว่าแม้ว่าตัวสร้างของคุณจะ "ว่าง" ในซอร์สโค้ด แต่คอนสตรัคเตอร์ที่คอมไพล์แล้วอาจมีโค้ดสำหรับเริ่มต้นสิ่งต่างๆเช่นตารางเสมือนและ RTTI ดังนั้นหน้าเว็บจะถูกสัมผัสในขณะก่อสร้างอยู่ดี
Wyzard

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

นั่นค่อนข้างทำให้ฉันประหลาดใจ คุณใช้ C ++ และ STL อะไร
David Thornley

3
ฉันเห็นด้วยกับคนอื่น ๆ ดูเหมือนว่าเป็นการใช้ std :: vector ที่ไม่ดี แม้ว่าวัตถุของคุณจะมี vtables แต่ก็จะไม่ถูกสร้างขึ้นจนกว่าคุณจะ push_back คุณควรจะทดสอบสิ่งนี้ได้โดยการประกาศตัวสร้างเริ่มต้นเป็นส่วนตัวเนื่องจากเวกเตอร์ทั้งหมดจะต้องมีคือตัวสร้างสำเนาสำหรับ push_back
ทอม

1
@David - การนำไปใช้บน AIX
EvilTeach

1

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

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


0

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


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

+1 Nils และเหตุการณ์เฉพาะอย่างหนึ่งของสิ่งนี้คือ "การกำหนดนามแฝง 64k" บนฮาร์ดแวร์ของ Intel
ทอม

นี่คือสิ่งที่พิสูจน์ได้ง่ายโดยดูจากการถอดชิ้นส่วนโดยวิธีการ เมื่อหลายปีก่อนฉันรู้สึกประหลาดใจที่ได้เห็นว่า gcc จะเพิ่มประสิทธิภาพการคูณคงที่ทุกประเภทด้วยการเลื่อนและการเพิ่มได้อย่างไร เช่นval * 7กลายเป็นสิ่งที่มีลักษณะเป็นอย่าง(val << 3) - valอื่น
ประ - เถิดเทิง

0

ใส่ฟังก์ชันขนาดเล็กและ / หรือที่เรียกบ่อยที่ด้านบนของไฟล์ต้นฉบับ นั่นทำให้คอมไพเลอร์หาโอกาสในการอินไลน์ได้ง่ายขึ้น


จริงๆ? คุณสามารถอ้างอิงเหตุผลและตัวอย่างสำหรับสิ่งนี้ได้หรือไม่? ไม่ได้บอกว่าไม่เป็นความจริงเพียง แต่ฟังดูไม่เข้าใจว่าสถานที่ตั้งจะมีความสำคัญ
underscore_d

@underscore_d มันไม่สามารถแทรกบางสิ่งบางอย่างได้จนกว่าจะทราบนิยามฟังก์ชัน ในขณะที่คอมไพเลอร์สมัยใหม่อาจสร้างการส่งผ่านหลายครั้งเพื่อให้ทราบคำจำกัดความในเวลาสร้างรหัส แต่ฉันไม่คิดว่ามัน
Mark Ransom

ฉันคิดว่าคอมไพเลอร์ทำงานจากกราฟการโทรนามธรรมแทนที่จะเป็นลำดับฟังก์ชันทางกายภาพซึ่งหมายความว่าสิ่งนี้จะไม่สำคัญ แน่นอนฉันคิดว่ามันไม่เจ็บที่จะต้องระมัดระวังเป็นพิเศษโดยเฉพาะอย่างยิ่งเมื่อประสิทธิภาพนอกเหนือจากนี้ IMO ดูเหมือนจะมีเหตุผลมากกว่าที่จะกำหนดฟังก์ชันที่ถูกเรียกก่อนหน้าที่จะเรียกพวกเขา ฉันต้องทดสอบประสิทธิภาพ แต่จะแปลกใจถ้ามันมีความสำคัญ แต่จนถึงตอนนั้นฉันรู้สึกประหลาดใจ!
underscore_d
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.