การยกเลิกโปรแกรมสำหรับขั้นตอนการทำงานใน CPU ตระกูล Intel Sandybridge


322

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

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

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

ที่ได้รับมอบหมายให้เลือกโปรแกรม Whetstone หรือ Monte-Carlo ความคิดเห็นแคชประสิทธิภาพส่วนใหญ่จะใช้เฉพาะกับ Whetstone แต่ฉันเลือกโปรแกรมจำลอง Monte-Carlo:

// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm>    // Needed for the "max" function
#include <cmath>
#include <iostream>

// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in 
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
  double x = 0.0;
  double y = 0.0;
  double euclid_sq = 0.0;

  // Continue generating two uniform random variables
  // until the square of their "euclidean distance" 
  // is less than unity
  do {
    x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    euclid_sq = x*x + y*y;
  } while (euclid_sq >= 1.0);

  return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}

// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(S_cur - K, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

// Pricing a European vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(K - S_cur, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

int main(int argc, char **argv) {
  // First we create the parameter list                                                                               
  int num_sims = 10000000;   // Number of simulated asset paths                                                       
  double S = 100.0;  // Option price                                                                                  
  double K = 100.0;  // Strike price                                                                                  
  double r = 0.05;   // Risk-free rate (5%)                                                                           
  double v = 0.2;    // Volatility of the underlying (20%)                                                            
  double T = 1.0;    // One year until expiry                                                                         

  // Then we calculate the call/put values via Monte Carlo                                                                          
  double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
  double put = monte_carlo_put_price(num_sims, S, K, r, v, T);

  // Finally we output the parameters and prices                                                                      
  std::cout << "Number of Paths: " << num_sims << std::endl;
  std::cout << "Underlying:      " << S << std::endl;
  std::cout << "Strike:          " << K << std::endl;
  std::cout << "Risk-Free Rate:  " << r << std::endl;
  std::cout << "Volatility:      " << v << std::endl;
  std::cout << "Maturity:        " << T << std::endl;

  std::cout << "Call Price:      " << call << std::endl;
  std::cout << "Put Price:       " << put << std::endl;

  return 0;
}

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


อัปเดต: อาจารย์ที่ให้งานนี้โพสต์รายละเอียด

ไฮไลท์คือ:

  • เป็นชั้นเรียนสถาปัตยกรรมภาคเรียนที่สองที่วิทยาลัยชุมชน (ใช้หนังสือเรียน Hennessy และ Patterson)
  • คอมพิวเตอร์แล็บมี Haswell CPUs
  • นักเรียนได้รับการCPUIDเรียนการสอนและวิธีการกำหนดขนาดแคชเช่นเดียวกับภายในและการCLFLUSHเรียนการสอน
  • ตัวเลือกคอมไพเลอร์ใด ๆ ที่ได้รับอนุญาตและเพื่อเป็น inline asm
  • การเขียนอัลกอริธึมสแควร์รูทของคุณเองได้รับการประกาศว่าอยู่นอกความซีด

ความคิดเห็นของ Cowmoogun เกี่ยวกับเมตาดาต้าเธรดระบุว่าการปรับแต่งคอมไพเลอร์ไม่ชัดเจนอาจเป็นส่วนหนึ่งของสิ่งนี้และสันนิษฐาน-O0ว่าการเพิ่มเวลาทำงาน 17% นั้นสมเหตุสมผล

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


โปรดทราบว่านี่เป็นคำถามเกี่ยวกับสถาปัตยกรรมคอมพิวเตอร์ไม่ใช่คำถามเกี่ยวกับวิธีทำให้ C ++ ช้าลงโดยทั่วไป


97
ฉันได้ยินมาว่า i7 นั้นแย่มากด้วยwhile(true){}
Cliff AB

3
จำนวน 2 ตู้เอทีเอ็ม HN: news.ycombinator.com/item?id=11749756
mlvljr

5
ด้วย openmp ถ้าคุณทำไม่ดีคุณควรที่จะสามารถทำให้เธรด N ใช้เวลานานกว่า 1
Flexo

9
คำถามนี้กำลังถูกอภิปรายในเมตาดาต้า
ผีของมาดาราระ

3
@bluefeet: ฉันเพิ่มว่าเพราะมันได้ดึงดูดหนึ่งปิดการลงคะแนนในภายใต้หนึ่งชั่วโมงของการเปิดอีกครั้ง ใช้เวลาเพียง 5 คนในการเข้าร่วมและ VTC โดยไม่ได้ตระหนักถึงความคิดเห็นในการอ่านเพื่อดูว่าอยู่ภายใต้การอภิปรายเกี่ยวกับเมตา ยังมีอีกโหวตอย่างใกล้ชิดตอนนี้ ฉันคิดว่าอย่างน้อยหนึ่งประโยคจะช่วยหลีกเลี่ยงการปิด / เปิดใหม่อีกรอบ
Peter Cordes

คำตอบ:


405

การอ่านเบื้องหลังที่สำคัญ: microarch pdf ของ Agner Fogและอาจเป็น Ulrich Drepper ด้วยอะไรทุกโปรแกรมเมอร์ควรรู้เกี่ยวกับหน่วยความจำ ดูลิงก์อื่น ๆ ในวิกิพีเดียแท็กโดยเฉพาะอย่างยิ่งคู่มือการเพิ่มประสิทธิภาพของ Intel และเดวิด Kanter ของการวิเคราะห์ของสถาปัตยกรรม Haswell กับแผนภาพ

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


ปัญหาเกี่ยวกับการใช้ถ้อยคำและรหัส :

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

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

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

สงครามและอุแว๊อันตรายสำหรับการลงทะเบียนจะสวยมากไม่เป็นปัญหาขอบคุณลงทะเบียนเปลี่ยนชื่อ (ยกเว้นสำหรับpopcnt/lzcnt/tzcntซึ่งมีการอ้างอิงที่ผิดพลาดในปลายทางของพวกเขาบน Intel CPUsแม้ว่าจะเป็นแบบเขียนอย่างเดียวเช่น WAW ที่ได้รับการจัดการเป็นอันตรายจาก RAW + การเขียน) สำหรับการสั่งซื้อหน่วยความจำซีพียูที่ทันสมัยใช้คิวร้านค้าในการกระทำความล่าช้าในแคชจนเกษียณยังหลีกเลี่ยงสงครามและอุแว๊อันตราย

ทำไม Mulge ใช้เวลาเพียง 3 รอบใน Haswell แตกต่างจากตารางคำแนะนำของ Agner มีข้อมูลเพิ่มเติมเกี่ยวกับการเปลี่ยนชื่อรีจิสเตอร์และการซ่อนเวลาแฝงของ FMA ในลูปผลิตภัณฑ์ดอท FP


ชื่อแบรนด์ "i7" ได้รับการแนะนำให้รู้จักกับ Nehalem (สืบต่อจาก Core2)และคู่มือ Intel บางคนยังพูดว่า "Core i7" เมื่อพวกเขาดูเหมือนจะหมายถึง Nehalem แต่พวกเขายังคงรักษาแบรนด์ "i7" ไว้สำหรับ Sandybridgeและ microarchitectures ภายหลัง SNB คือเมื่อ P6 ครอบครัวกลายเป็นสายพันธุ์ใหม่ที่ SNB-ครอบครัว ในหลาย ๆ ทาง Nehalem มีส่วนร่วมกับ Pentium III มากกว่ากับ Sandybridge (เช่น register read stalls และ ROB-read ไม่ได้เกิดขึ้นกับ SnB เพราะมันเปลี่ยนเป็นการใช้ไฟล์ลงทะเบียนทางกายภาพนอกจากนี้ uop cache และ Internal รูปแบบ uop) คำว่า "สถาปัตยกรรม i7" ไม่มีประโยชน์เพราะมันสมเหตุสมผลเล็กน้อยที่จะจัดกลุ่ม SnB-family กับ Nehalem แต่ไม่ใช่ Core2 (Nehalem ได้แนะนำสถาปัตยกรรมแคช L3 แบบรวมที่ใช้ร่วมกันสำหรับการเชื่อมต่อหลายคอร์ด้วยกันและรวมถึง GPU ด้วยดังนั้นระดับชิปจึงทำให้การตั้งชื่อเหมาะสมยิ่งขึ้น)


บทสรุปของความคิดที่ดีที่การไร้ความสามารถที่โหดร้ายสามารถพิสูจน์ได้

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

  • มัลติเธรดพร้อมตัวนับลูปที่แชร์ เดียวstd::atomic<uint64_t>ดังนั้นจำนวนการวนซ้ำที่ถูกต้องจึงเกิดขึ้น uint64_t -m32 -march=i586อะตอมเป็นที่ไม่ดีโดยเฉพาะอย่างยิ่งกับ สำหรับคะแนนโบนัสจัดเรียงให้ตรงแนวและข้ามขอบเขตของหน้าด้วยการแบ่งที่ไม่สม่ำเสมอ (ไม่ใช่ 4: 4)
  • การแชร์เท็จสำหรับตัวแปรที่ไม่ใช่อะตอมมิกอื่น ๆ -> ไปป์ไลน์การเก็งกำไรการสั่งซื้อผิดพลาดของหน่วยความจำจะถูกลบออกไป
  • แทนการใช้-กับตัวแปร FP, XOR ไบต์สูงกับ 0x80 พลิกบิตเครื่องหมายที่ก่อให้เกิดแผงลอยร้านค้าส่งต่อ
  • RDTSCเวลาแต่ละซ้ำอิสระกับสิ่งที่ยิ่งหนักกว่า เช่นCPUID/ RDTSCหรือฟังก์ชั่นเวลาที่ทำให้การเรียกระบบ คำแนะนำในการซีเรียลไลซ์
  • เปลี่ยนค่าคูณด้วยค่าคงที่เพื่อหารด้วยส่วนกลับ ("เพื่อความสะดวกในการอ่าน") div ช้าและไม่ได้ถูกวางท่ออย่างเต็มที่
  • vectorize คูณ / sqrt กับ AVX (SIMD) แต่ล้มเหลวในการใช้งานvzeroupperก่อนที่จะโทรไปเกลาคณิตศาสตร์ห้องสมุดexp()และlog()ฟังก์ชั่นที่ก่อให้เกิดAVX <-> SSE แผงลอยเปลี่ยนแปลง
  • เก็บเอาต์พุต RNG ในรายการที่เชื่อมโยงหรือในอาร์เรย์ที่คุณท่องไปตามลำดับ เหมือนกันสำหรับผลลัพธ์ของการวนซ้ำแต่ละครั้งและหาผลรวมในตอนท้าย

ยังครอบคลุมในคำตอบนี้ แต่ไม่รวมอยู่ในบทสรุป: คำแนะนำที่จะช้าเท่ากับซีพียูที่ไม่ได้ทำงานผิดพลาดหรือไม่ดูเหมือนว่าจะสมเหตุสมผลได้แม้จะมีความสามารถในการก่อวินาศกรรม เช่นแนวคิด gimp-the-compiler จำนวนมากที่สร้าง asm ที่แตกต่างกัน / แย่ลงอย่างเห็นได้ชัด


หลายเธรดไม่ดี

อาจใช้ OpenMP กับลูปแบบหลายเธรดที่มีการวนซ้ำน้อยมากโดยมีค่าใช้จ่ายมากกว่าการเพิ่มความเร็ว รหัส monte-carlo ของคุณมีความเท่าเทียมกันมากพอที่จะได้รับความเร็ว หากเราประสบความสำเร็จในการทำให้การทำซ้ำแต่ละครั้งช้าลง (แต่ละเธรดคำนวณส่วนที่payoff_sumเพิ่มเข้ามาในตอนท้าย) #omp parallelในลูปนั้นน่าจะเป็นการเพิ่มประสิทธิภาพไม่ใช่การมองดูในแง่ร้าย

หลายเธรด แต่บังคับให้เธรดทั้งสองแบ่งใช้ตัวนับลูปเดียวกัน (พร้อมatomicเพิ่มขึ้นดังนั้นจำนวนการวนซ้ำทั้งหมดจึงถูกต้อง) ดูเหมือนว่ามีเหตุผลที่โหดร้าย นี่หมายถึงการใช้staticตัวแปรเป็นตัวนับลูป justifies นี้ใช้atomicสำหรับเคาน์เตอร์ห่วงและสร้างจริงแคชเส้น ping-ponging (ตราบเท่าที่หัวข้อไม่ทำงานบนหลักกายภาพเดียวกันกับ hyperthreading นั่นอาจจะไม่เป็นช้า) อย่างไรก็ตามนี้เป็นมากlock incช้ากว่ากรณียกเลิกการเกี่ยงสำหรับ และlock cmpxchg8bเพื่อเพิ่มอะตอมเกี่ยงuint64_tบนระบบ 32 incบิตจะต้องลองใหม่อีกครั้งในวงแทนที่จะต้องฮาร์ดแวร์ตัดสินอะตอม

สร้างการแบ่งปันที่ไม่ถูกต้องโดยที่หลาย ๆ เธรดเก็บข้อมูลส่วนตัว (เช่นสถานะ RNG) ในไบต์ที่ต่างกันของบรรทัดแคชเดียวกัน (Intel กวดวิชาเกี่ยวกับเรื่องนี้รวมทั้งเคาน์เตอร์ perf ไปดูที่) มีลักษณะสถาปัตยกรรมเฉพาะนี้เป็น : CPU ของ Intel เก็งกำไรในหน่วยความจำที่ผิดพลาดการสั่งซื้อไม่ได้เกิดขึ้นและมีเครื่องล้างหน่วยความจำการสั่งซื้อเหตุการณ์ perf ในการตรวจสอบเรื่องนี้อย่างน้อยใน P4 การลงโทษอาจไม่ใหญ่เท่าแฮสเวลล์ เมื่อลิงก์ชี้ให้เห็นlockคำสั่ง ed จะถือว่าเกิดขึ้นโดยหลีกเลี่ยงการเก็งกำไรที่ไม่ถูกต้อง โหลดปกติคาดการณ์ว่าแกนอื่น ๆ จะไม่ทำให้แคชในบรรทัดระหว่างเมื่อโหลดดำเนินการและเมื่อมันเกษียณในการสั่งซื้อโปรแกรม (นอกเสียจากคุณจะใช้pause ) การแบ่งปันที่แท้จริงโดยไม่มีlockคำแนะนำ ed มักเป็นข้อบกพร่อง มันน่าสนใจที่จะเปรียบเทียบตัวนับลูปแบบไม่ใช้อะตอมร่วมกับเคสอะตอม หากต้องการลดขนาดจริง ๆ ให้เก็บตัวนับอะตอมมิกแบบแบ่งใช้และทำให้การแบ่งปันแบบเท็จในบรรทัดแคชเดียวกันหรือแตกต่างกันสำหรับตัวแปรอื่น ๆ


ความคิดเฉพาะ uarch แบบสุ่ม:

หากคุณสามารถแนะนำสาขาที่ไม่สามารถคาดเดาได้สิ่งนั้นจะทำให้รหัสเสื่อมสภาพลงอย่างมาก ซีพียู x86 รุ่นใหม่มีท่อค่อนข้างยาวดังนั้นค่าใช้จ่ายที่ผิดพลาดประมาณ 15 รอบ (เมื่อเรียกใช้จากแคช uop)


โซ่พึ่งพา:

ฉันคิดว่านี่เป็นส่วนหนึ่งของงานที่ได้รับมอบหมาย

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

ในการทำให้สิ่งนี้มีประสิทธิภาพจริงๆให้เพิ่มความยาวของห่วงโซ่การพึ่งพาแบบวนรอบที่ดำเนินการ ไม่มีอะไรกระโดดออกมาอย่างชัดเจนแม้ว่า: ลูปตามที่เขียนมีห่วงโซ่การพึ่งพาอาศัยแบบห่วงสั้นมาก: เพียงแค่เพิ่ม FP (3 รอบ) การวนซ้ำหลายครั้งสามารถคำนวณได้ในคราวเดียวเนื่องจากสามารถเริ่มต้นได้ดีก่อนpayoff_sum +=สิ้นสุดการทำซ้ำครั้งก่อน ( log()และexpทำตามคำแนะนำหลายอย่าง แต่ไม่เกินหน้าต่างออกคำสั่งของ Haswell สำหรับการค้นหาความเท่าเทียม: ROB ขนาด = 192 u fused โดเมนโดเมนและขนาดตัวกำหนดตารางเวลา = uops โดเมนที่ไม่ได้ใช้ 60 ครั้ง. ทันทีที่การประมวลผลของการทำซ้ำปัจจุบันดำเนินไปไกลพอที่จะทำให้มีที่ว่างสำหรับคำแนะนำจากการทำซ้ำครั้งต่อไปที่จะออกชิ้นส่วนใด ๆ ของมันที่มีอินพุตพร้อม (เช่นแยกอิสระ / แยกโซ่) สามารถเริ่มดำเนินการได้ ฟรี (เช่นเนื่องจากมีปัญหาเรื่องคอขวดเนื่องจากความล่าช้าไม่ใช่ปริมาณงาน)

รัฐ RNG addpsเกือบจะแน่นอนจะเป็นห่วงโซ่ดำเนินการพึ่งพานานกว่า


ใช้การดำเนินการ FP ช้าลง / มากขึ้น

หารด้วย 2.0 แทนการคูณ 0.5 และอื่น ๆ FP multiply มีการวางท่ออย่างหนักในการออกแบบของ Intel และมีหนึ่งอัตราต่อ 0.5c สำหรับ Haswell และใหม่กว่า FP divsd/divpdเป็นไปป์ไลน์เพียงบางส่วนเท่านั้น (แม้ว่า Skylake จะมีความน่าประทับใจในอัตราการส่งผ่านข้อมูล 4c divpd xmmแต่ด้วยความหน่วงแฝง 13-14c เทียบกับ Nehalem (7-22c)

การdo { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);ทดสอบระยะไกลนั้นชัดเจนดังนั้นมันจะเหมาะสมกับsqrt()มัน : P ( sqrtช้ากว่าdiv)

ตามที่ @Paul Clayton แนะนำการเขียนนิพจน์ด้วยการเชื่อมโยง / การแจกแจงแบบเทียบเท่าสามารถแนะนำการทำงานเพิ่มเติมได้ (ตราบใดที่คุณไม่ได้ใช้-ffast-mathเพื่ออนุญาตให้คอมไพเลอร์ทำการปรับให้เหมาะสมอีกครั้ง) อาจจะกลายเป็น(exp(T*(r-0.5*v*v)) exp(T*r - T*v*v/2.0)โปรดทราบว่าในขณะที่การคำนวณทางคณิตศาสตร์เกี่ยวกับจำนวนจริงนั้นมีความสัมพันธ์กัน แต่คณิตศาสตร์จุดลอยตัวไม่ได้แม้ว่าจะไม่ได้พิจารณา overflow / NaN -ffast-mathก็ตาม ดูความคิดเห็นของ Paulสำหรับpow()คำแนะนำที่ซ้อนกันมีขนดกมาก

หากคุณสามารถปรับขนาดการคำนวณลงไปขนาดเล็กจำนวนมากแล้ว Ops คณิตศาสตร์ FP ใช้~ 120 รอบพิเศษในการดักเฟิเมื่อการดำเนินการเกี่ยวกับตัวเลขสองปกติผลิต denormal ดู microarch PDF ของ Agner Fog สำหรับหมายเลขและรายละเอียดที่แน่นอน สิ่งนี้ไม่น่าเป็นไปได้เนื่องจากคุณมีทวีคูณจำนวนมากดังนั้นตัวคูณสเกลจะถูกยกกำลังสองและต่ำไปจนถึง 0.0 ฉันไม่เห็นวิธีที่จะปรับขนาดที่จำเป็นด้วยการไร้ความสามารถ (แม้กระทั่งความโหดร้าย) เพียงความอาฆาตพยาบาทโดยเจตนา


หากคุณสามารถใช้อินทรินสิก ( <immintrin.h>)

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

ใช้จำนวนเต็มสับเปลี่ยนระหว่างการดำเนินการทางคณิตศาสตร์ FP เพื่อทำให้เกิดการบายพาสล่าช้า

การผสมคำสั่ง SSE และ AVX โดยไม่ต้องใช้vzeroupperสาเหตุที่เหมาะสมแผงลอยขนาดใหญ่ใน pre-Skylake (และการลงโทษที่แตกต่างกันใน Skylake ) ถึงแม้ว่าจะไม่มีการทำให้ vectorizing แย่ลงกว่าสเกลาร์ (รอบเพิ่มเติมที่ใช้ข้อมูลสับเข้า / ออกของเวกเตอร์มากกว่าที่บันทึกไว้โดยการดำเนินการเพิ่ม / sub / mul / div / sqrt สำหรับการทำซ้ำ 4 Monte-Carlo ในครั้งเดียว . เพิ่ม / ย่อย / หน่วยปฏิบัติมัลไปป์ไลน์กำลังอย่างเต็มที่และเต็มความกว้าง แต่ div และ sqrt บน 256b เวกเตอร์ไม่ได้เป็นอย่างรวดเร็วบน 128b เวกเตอร์ (หรือสเกลา) doubleเพื่อเพิ่มความเร็วไม่ได้เป็นอย่างมากสำหรับ

exp()และlog()ไม่มีการรองรับฮาร์ดแวร์ดังนั้นส่วนนั้นจะต้องแยกองค์ประกอบเวกเตอร์กลับไปที่สเกลาร์และเรียกใช้ฟังก์ชันไลบรารีแยกต่างหากจากนั้นจึงสับผลลัพธ์กลับเป็นเวกเตอร์ โดยทั่วไปแล้ว libm จะรวบรวมเพื่อใช้ SSE2 เท่านั้นดังนั้นจะใช้การเข้ารหัสแบบดั้งเดิม - SSE ของคำสั่งทางคณิตศาสตร์สเกลาร์ หากรหัสของคุณใช้พาหะและการโทร 256b expโดยไม่ทำการโทรvzeroupperก่อน หลังจากกลับมาคำสั่ง AVX-128 ต้องการvmovsdตั้งค่าองค์ประกอบเวกเตอร์ถัดไปเป็นอาร์กิวเมนต์สำหรับexpจะหยุดชะงัก จากนั้นexp()จะหยุดอีกครั้งเมื่อเรียกใช้คำสั่ง SSE นี่คือสิ่งที่เกิดขึ้นในคำถามนี้ทำให้เกิดการชะลอตัว 10 เท่า (ขอบคุณ @ZBoson)

ดูการทดลองของ Nathan Kurz กับ lib คณิตศาสตร์กับ glibc ของ Intel สำหรับโค้ดนี้นี้ glibc ในอนาคตจะมาพร้อมกับการใช้งานแบบเวกเตอร์exp()และอื่น ๆ


หากกำหนดเป้าหมายล่วงหน้า IvB หรือ esp ตอนนี้ลองรับ gcc เพื่อทำให้แผงลอยลงทะเบียนบางส่วนกับการดำเนินการ 16 บิตหรือ 8 บิตตามด้วยการดำเนินงาน 32 บิตหรือ 64 บิต ในกรณีส่วนใหญ่ gcc จะใช้movzxหลังจากการดำเนินการ 8 หรือ 16 บิต แต่นี่เป็นกรณีที่ gcc แก้ไขahแล้วอ่านax


ด้วย asm (inline):

ด้วย asm (inline) คุณสามารถแบ่งแคช uop: โค้ด 32B ที่ไม่พอดีกับแคช 6uop สามบรรทัดบังคับให้สวิตช์จาก uop cache ไปยังตัวถอดรหัส คนไร้ความสามารถที่ALIGNใช้ไบต์เดี่ยวจำนวนมากnopแทนที่จะยาวสองสามnopวินาทีบนเป้าหมายสาขาภายในวงด้านในอาจทำการหลอก หรือใส่การจัดตำแหน่งช่องว่างด้านหลังฉลากแทนที่จะเป็นมาก่อน : P สิ่งนี้จะสำคัญก็ต่อเมื่อส่วนหน้าเป็นคอขวดซึ่งจะไม่เกิดขึ้นถ้าเราประสบความสำเร็จในการมองดูส่วนที่เหลือของรหัส

ใช้รหัสการแก้ไขด้วยตนเองเพื่อทริกเกอร์การล้างข้อมูลไปป์ไลน์ (aka machine-nukes)

LCP แผงลอยจากคำสั่ง 16 บิตที่มีขนาดใหญ่เกินไปในทันทีเพื่อให้พอดีกับ 8 บิตไม่น่าจะมีประโยชน์ แคช uop บน SnB และใหม่กว่าหมายความว่าคุณจ่ายค่าปรับการถอดรหัสเพียงครั้งเดียว บน Nehalem (i7 แรก) อาจทำงานได้กับลูปที่ไม่พอดีกับบัฟเฟอร์ 28 uop gcc บางครั้งจะสร้างคำแนะนำดังกล่าวแม้จะมี-mtune=intelและเมื่อมันสามารถใช้คำสั่ง 32 บิต


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


ทำให้แคชหายไปจำนวนมากและหน่วยความจำช้าลง

ใช้union { double d; char a[8]; }สำหรับตัวแปรบางตัวของคุณ ทำให้แผงลอยการส่งต่อร้านค้าโดยทำร้านแคบ (หรืออ่าน - แก้ไข - เขียน) ให้เป็นหนึ่งในไบต์ (บทความ wiki นั้นยังครอบคลุมเนื้อหา microarchitectural อื่น ๆ อีกมากมายสำหรับการโหลด / จัดคิว) เช่นพลิกสัญลักษณ์ของการdoubleใช้ XOR 0x80 บนไบต์สูงแทนที่จะใช้-ตัวดำเนินการ ผู้พัฒนาที่ไร้ความสามารถอย่างบ้าคลั่งอาจได้ยินว่า FP ช้ากว่าจำนวนเต็มและพยายามทำมากที่สุดโดยใช้ ops จำนวนเต็ม (คอมไพเลอร์ที่ดีมากที่กำหนดเป้าหมายทางคณิตศาสตร์ FP ในการลงทะเบียน SSE อาจจะรวบรวมสิ่งนี้กับxorps ด้วยค่าคงที่ในการลงทะเบียน xmm อื่น แต่วิธีเดียวที่ไม่น่ากลัวสำหรับ x87 คือถ้าคอมไพเลอร์ตระหนักว่ามันเป็นการลบค่าและแทนที่การบวกถัดไปด้วยการลบ)


ใช้volatileถ้าคุณกำลังคอมไพล์ด้วย-O3และไม่ได้ใช้std::atomicเพื่อบังคับให้คอมไพเลอร์เก็บ / โหลดซ้ำทั่วสถานที่จริง ตัวแปรทั่วโลก (แทนที่จะเป็นคนในพื้นที่) จะบังคับให้ร้านค้า / รีโหลดบางส่วน แต่การสั่งซื้อที่อ่อนแอของรุ่น C + + หน่วยความจำไม่จำเป็นต้องให้คอมไพเลอร์รั่วไหล / โหลดซ้ำไปยังหน่วยความจำตลอดเวลา

แทนที่ vars โลคัลด้วยสมาชิกของ struct ขนาดใหญ่เพื่อให้คุณสามารถควบคุมโครงร่างหน่วยความจำ

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

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

ยิ่งไปกว่านั้นนำสิ่งที่ตรง 4096B กันตั้งแต่โหลดมีการพึ่งพาที่ผิดพลาดในร้านค้าไปยังหน้าเว็บที่แตกต่างกัน แต่มีเดียวกันชดเชยภายในหน้า ก้าวร้าวออกจากการสั่งซื้อซีพียูใช้หน่วยความจำ Disambiguation ที่จะคิดออกเมื่อโหลดและร้านค้าสามารถจัดลำดับใหม่โดยไม่ต้องเปลี่ยนผลและการดำเนินการของอินเทลมีเท็จบวกที่ป้องกันไม่ให้โหลดเริ่มต้น อาจเป็นเพียงการตรวจสอบบิตด้านล่างออฟเซ็ตของหน้าดังนั้นการตรวจสอบจึงสามารถเริ่มต้นได้ก่อนที่ TLB จะแปลบิตสูงจากเพจเสมือนเป็นเพจจริง เช่นเดียวกับมัคคุเทศก์ของ Agner ให้ดูคำตอบจาก Stephen Canonและหมวดใกล้กับคำตอบของ @Krazy Glew ในคำถามเดียวกัน (Andy Glew เป็นหนึ่งในสถาปนิกของ P6 microar Architecture ดั้งเดิมของ Intel)

ใช้__attribute__((packed))เพื่อให้คุณจัดแนวตัวแปรที่ไม่ถูกต้องเพื่อให้พวกเขาขยายขอบเขตแคชหรือแม้แต่ขอบเขตของหน้า (ดังนั้นการโหลดหนึ่งdoubleต้องการข้อมูลจากสองบรรทัดแคช) โหลดที่ไม่ตรงแนวไม่มีโทษใน Intel i7 uarch ใด ๆ ยกเว้นเมื่อข้ามบรรทัดแคชและบรรทัดของหน้า การแยกแคชบรรทัดยังคงใช้รอบเพิ่มเติม Skylake ลดบทลงโทษสำหรับการโหลดแยกหน้าอย่างมากจาก 100 เป็น 5 รอบ (ส่วน 2.1.3) อาจเกี่ยวข้องกับความสามารถในการทำสองหน้าเดินขนานกัน

การแบ่งหน้าบนatomic<uint64_t>ควรเป็นกรณีที่เลวร้ายที่สุดโดยเฉพาะ ถ้ามันเป็น 5 ไบต์ในหนึ่งหน้าและ 3 ไบต์ในอีกหน้าหรืออย่างอื่นที่ไม่ใช่ 4: 4 แม้การแยกลงตรงกลางจะมีประสิทธิภาพมากขึ้นสำหรับการแบ่งแคชกับเวกเตอร์ 16B ในบางระบบ IIRC ใส่ทุกอย่างไว้ในalignas(4096) struct __attribute((packed))(เพื่อประหยัดพื้นที่แน่นอน) รวมถึงอาร์เรย์สำหรับจัดเก็บข้อมูลสำหรับผลลัพธ์ RNG บรรลุแนวที่ไม่ถูกต้องโดยใช้uint8_tหรือuint16_tทำอะไรบางอย่างก่อนเคาน์เตอร์

ถ้าคุณได้รับคอมไพเลอร์ที่จะใช้จัดทำดัชนีที่อยู่โหมดที่จะเอาชนะ UOP ไมโครฟิวชั่น อาจจะโดยการใช้เพื่อแทนที่ตัวแปรสเกลาง่ายด้วย#definemy_data[constant]

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


ทราเวิร์อาร์เรย์ในลำดับที่ไม่ต่อเนื่องกัน

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

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

เพื่อให้ได้ผลในแง่ร้ายสูงสุดให้วนลูปมากกว่าอาร์เรย์ของคุณด้วยความยาว 4096 ไบต์ (เช่น 512 doubles) เช่น

for (int i=0 ; i<512; i++)
    for (int j=i ; j<UPPER_BOUND ; j+=512)
        monte_carlo_step(rng_array[j]);

รูปแบบการเข้าถึงคือ 0, 4096, 8192, ... ,
8, 4104, 8200, ...
16, 4112, 8208, ...

นี่คือสิ่งที่คุณจะได้รับจากการเข้าถึงอาร์เรย์ 2 มิติตามลำดับdouble rng_array[MAX_ROWS][512]ที่ไม่ถูกต้อง (วนซ้ำแถวแทนที่จะเป็นคอลัมน์ภายในหนึ่งแถวในวงในตามที่แนะนำโดย @JesperJuhl) หากการไร้ความสามารถที่โหดร้ายสามารถแสดงให้เห็นถึงอาเรย์ 2 มิติที่มีขนาดเช่นนั้นการที่ความหลากหลายของสวนในโลกแห่งความเป็นจริงนั้นง่ายต่อการวนซ้ำกับรูปแบบการเข้าถึงที่ผิด สิ่งนี้เกิดขึ้นในรหัสจริงในชีวิตจริง

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

สิ่งนี้จะสร้างการพลาด TLB จำนวนมากเว้นแต่หน้าจะถูกรวมเข้ากับ hugepage ( Linux จะทำเช่นนี้เพื่อการจัดสรรที่ไม่ระบุชื่อ (ไม่ใช่ไฟล์ที่สำรองข้อมูล) เช่นmalloc/ การnewใช้งานmmap(MAP_ANONYMOUS) )

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


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

ค่อนข้างปิดหัวข้อ: ทำให้คอมไพเลอร์สร้างรหัสที่แย่ลง / ทำงานได้มากขึ้น:

ใช้ C ++ 11 std::atomic<int>และstd::atomic<double>สำหรับรหัสที่ผิดพลาดมากที่สุด MFENCEs และlockคำแนะนำ ed ค่อนข้างช้าแม้จะไม่มีการโต้แย้งจากเธรดอื่น

-m32จะทำให้รหัสช้าลงเนื่องจากรหัส x87 จะแย่กว่ารหัส SSE2 32bit เรียกประชุมสแต็คตามคำแนะนำจะใช้เวลามากขึ้นและผ่าน args FP exp()แม้ในกองการทำงานเช่น atomic<uint64_t>::operator++เปิด-m32ต้องการlock cmpxchg8Bลูป (i586) (ใช้มันเพื่อเคาน์เตอร์ลูป!

-march=i386จะทำให้ลดน้อยลง (ขอบคุณ @Jesper) FP เปรียบเทียบกับfcomจะช้ากว่า fcomi686 Pre-586 ไม่ได้จัดเก็บ atomic 64 บิต (นับประสา cmpxchg) ดังนั้น 64 บิตatomicops ทั้งหมดจึงรวบรวมการเรียกใช้ฟังก์ชั่น libgcc (ซึ่งอาจจะรวบรวมสำหรับ i686 แทนที่จะใช้ล็อคจริง) ลองใช้ลิงค์ Godbolt Compiler Explorer ในย่อหน้าสุดท้าย

ใช้long double/ sqrtl/ explเพื่อความแม่นยำและความช้าพิเศษใน ABIs โดยที่ sizeof ( long double) เท่ากับ 10 หรือ 16 (พร้อมการเว้นระยะห่างสำหรับการจัดตำแหน่ง) (IIRC, 64 บิต Windows ใช้ 8byte long doubleเทียบเท่ากับdouble(โหลด / เก็บ 10byte (80 บิต) ตัวถูกดำเนินการ FP คือ 4/7 uops เทียบกับfloatหรือdoubleรับเพียง 1 uop ต่อfld m64/m32/ fst) บังคับ x87 ด้วยการlong doubleเอาชนะ auto-vectorization แม้ -m64 -march=haswell -O3GCC

หากไม่ได้ใช้atomic<uint64_t>ตัวนับลูปให้ใช้long doubleกับทุกอย่างรวมถึงตัวนับลูป

atomic<double>คอมไพล์ แต่การดำเนินการอ่าน - แก้ไข - เขียนเช่น+=ไม่ได้รับการสนับสนุน (แม้ใน 64 บิต) atomic<long double>ต้องเรียกใช้ฟังก์ชันไลบรารีสำหรับโหลด / เก็บอะตอมเท่านั้น มันอาจไม่มีประสิทธิภาพจริง ๆเพราะ x86 ISA ไม่รองรับการโหลด / ร้านค้าขนาด 10byte ของอะตอมและวิธีเดียวที่ฉันสามารถคิดได้โดยไม่ต้องล็อก ( cmpxchg16b) ต้องใช้โหมด 64 บิต


ที่การ-O0สลายการแสดงออกโดยการกำหนดชิ้นส่วนให้กับ vars ชั่วคราวจะทำให้ร้านค้า / โหลดซ้ำมากขึ้น หากไม่มีvolatileสิ่งใดสิ่งนี้จะไม่สำคัญกับการตั้งค่าการปรับให้เหมาะสมที่รหัสจริงของจริงจะใช้

C กฎ aliasing อนุญาตให้charไปยังนามแฝงอะไรดังนั้นการจัดเก็บผ่านchar*กองกำลังรวบรวมเพื่อทุกอย่าง / ร้านโหลดก่อน / -O3หลังไบต์ร้านได้ที่ (นี่เป็นปัญหาสำหรับรหัสการทำให้เป็นuint8_tเวกเตอร์อัตโนมัติที่ทำงานกับอาเรย์ของเป็นต้น)

ลองใช้uint16_tตัวนับลูปเพื่อบังคับให้มีการตัดเป็น 16 บิตโดยใช้ขนาดตัวถูกดำเนินการ 16 บิต (แผงควบคุมที่เป็นไปได้) และ / หรือmovzxคำแนะนำพิเศษ(ปลอดภัย) ลงนามล้นเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ดังนั้นถ้าคุณใช้-fwrapvหรืออย่างน้อย-fno-strict-overflow, เคาน์เตอร์ลงนามวงจะได้ไม่ต้องเป็นอีกสัญญาณที่ขยายทุกซ้ำแม้ว่าใช้เป็นชดเชยที่จะชี้ 64bit


บังคับการแปลงจากจำนวนเต็มเป็นfloatและย้อนกลับอีกครั้ง และ / หรือdouble<=> floatการแปลง คำแนะนำมีเวลาแฝงมากกว่าหนึ่งและ scalar int-> float ( cvtsi2ss) ได้รับการออกแบบมาไม่ดีเพื่อไม่ให้ส่วนที่เหลือของการลงทะเบียน xmm เป็นศูนย์ (gcc แทรกส่วนเพิ่มเติมpxorเพื่อแยกการขึ้นต่อกันด้วยเหตุนี้)


ตั้งค่า CPU affinity ของคุณเป็น CPU อื่นบ่อยครั้ง(แนะนำโดย @Egwor) การใช้เหตุผลที่โหดร้าย: คุณไม่ต้องการให้แกนใดแกนหนึ่งร้อนเกินไปจากการรันเธรดของคุณเป็นเวลานานใช่ไหม? บางทีการเปลี่ยนไปใช้คอร์อื่นจะทำให้คอร์นั้นมีความเร็วสัญญาณนาฬิกาสูงขึ้น (ในความเป็นจริง: พวกมันใกล้กันมากจนแทบไม่น่าเป็นไปได้ยกเว้นในระบบมัลติซ็อกเก็ต) ตอนนี้เพิ่งปรับจูนผิดและทำบ่อยเกินไป นอกเหนือจากเวลาที่ใช้ในการบันทึก / เรียกคืนสถานะเธรด OS แกนใหม่นั้นมีแคช L2 / L1 เย็นแคช uop และตัวพยากรณ์สาขา

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

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


10
@JesperJuhl: ใช่ฉันจะซื้อข้ออ้างนั้น "diabolically ไร้ความสามารถ" เป็นเช่นวลีที่ยอดเยี่ยม :)
ปีเตอร์ Cordes

2
การเปลี่ยนการคูณด้วยค่าคงที่เป็นการหารด้วยค่าผกผันของค่าคงที่อาจลดประสิทธิภาพลงเล็กน้อย (อย่างน้อยถ้าไม่มีใครพยายามเอาชนะ -O3-fastmath) ในทำนองเดียวกันโดยใช้การเชื่อมโยงกันกับการเพิ่มการทำงาน ( exp(T*(r-0.5*v*v))กลายexp(T*r - T*v*v/2.0); exp(sqrt(v*v*T)*gauss_bm)กลายเป็นexp(sqrt(v)*sqrt(v)*sqrt(T)*gauss_bm)) การเชื่อมโยง (และการวางนัยทั่วไป) สามารถแปลงexp(T*r - T*v*v/2.0)เป็น `pow ((pow (e_value, T), r) / pow (pow (pow (pow (e_value, T), v), v)), - 2.0) [หรือบางอย่าง เช่นนั้น] เทคนิคทางคณิตศาสตร์เช่นนั้นไม่นับว่าเป็นการลดความเสี่ยงทางจุลภาค
Paul A. Clayton

2
ฉันซาบซึ้งกับคำตอบนี้มากและหมอกของ Agner ก็ช่วยได้มาก ฉันจะปล่อยให้ย่อยนี้และเริ่มทำงานในบ่ายวันนี้ นี่อาจเป็นการมอบหมายที่มีประโยชน์ที่สุดในแง่ของการเรียนรู้สิ่งที่เกิดขึ้นจริง
Cowmoogun

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

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

35

บางสิ่งที่คุณสามารถทำได้เพื่อทำให้สิ่งต่าง ๆ ทำงานได้ไม่ดีเท่าที่จะทำได้:

  • รวบรวมรหัสสำหรับสถาปัตยกรรม i386 สิ่งนี้จะป้องกันการใช้ SSE และคำแนะนำที่ใหม่กว่าและบังคับให้ใช้ x87 FPU

  • ใช้std::atomicตัวแปรทุกที่ สิ่งนี้จะทำให้พวกเขามีราคาแพงมากเนื่องจากคอมไพเลอร์ที่ถูกบังคับให้ใส่อุปสรรคหน่วยความจำทั่วทุกสถานที่ และนี่คือสิ่งที่คนไร้ความสามารถอาจทำเพื่อ "มั่นใจในความปลอดภัยของกระทู้"

  • ตรวจสอบให้แน่ใจว่าเข้าถึงหน่วยความจำด้วยวิธีที่เป็นไปได้ที่แย่ที่สุดสำหรับ prefetcher ในการทำนาย

  • เพื่อให้ตัวแปรของคุณมีราคาแพงเป็นพิเศษคุณสามารถตรวจสอบให้แน่ใจว่าพวกเขาทั้งหมดมี 'ระยะเวลาการจัดเก็บแบบไดนามิก' (จัดสรรฮีป) โดยจัดสรรให้newแทนที่จะปล่อยให้พวกเขามี

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

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

หมายเหตุ: คำตอบนี้เป็นเพียงแค่สรุปความคิดเห็นของฉันที่ @Peter Cordes รวมอยู่ในคำตอบที่ดีของเขาแล้ว แนะนำให้เขาได้รับการ upvote ของคุณหากคุณมีเพียงหนึ่งที่จะสำรอง :)


9
คัดค้านหลักของฉันกับบางส่วนของเหล่านี้เป็นถ้อยคำของคำถาม: การ deoptimize โปรแกรมที่ใช้ความรู้ของวิธีการที่ท่อ i7 Intel ดำเนินการ ฉันไม่รู้สึกว่ามีสิ่งใดที่เฉพาะเจาะจงเกี่ยวกับ x87 หรือstd::atomicหรือระดับทางอ้อมเพิ่มเติมจากการจัดสรรแบบไดนามิก พวกมันจะช้าลงใน Atom หรือ K8 เช่นกัน ยังคงมีการอัปเดต แต่นั่นเป็นสาเหตุที่ฉันต่อต้านคำแนะนำของคุณบางส่วน
Peter Cordes

นั่นคือจุดที่ยุติธรรม ไม่ว่าสิ่งเหล่านั้นยังคงทำงานต่อเป้าหมายของผู้ถามบ้าง ขอบคุณการโหวต :)
Jesper Juhl

หน่วย SSE ใช้พอร์ต 0, 1 และ 5 หน่วย x87 ใช้เฉพาะพอร์ต 0 และ 1
Michas

@Michas: คุณผิดเกี่ยวกับเรื่องนี้ Haswell ไม่ได้รันคำสั่งทางคณิตศาสตร์ SSE FP ใด ๆ บนพอร์ต 5 ส่วนใหญ่เป็น shuffles และบูลีน SSE FP (xorps / andps / orps) x87 ช้าลง แต่คำอธิบายของคุณว่าทำไมจึงผิดเล็กน้อย (และจุดนี้ผิดอย่างสมบูรณ์)
Peter Cordes

1
@Michas: movapd xmm, xmmโดยปกติแล้วไม่จำเป็นต้องมีพอร์ตดำเนินการ (จัดการในขั้นตอนการลงทะเบียนเปลี่ยนชื่อบน IVB และใหม่กว่า) นอกจากนี้ยังแทบไม่เคยต้องการรหัส AVX เพราะทุกอย่างยกเว้น FMA นั้นไม่ทำลาย แต่ยุติธรรมเพียงพอ Haswell รันบนพอร์ต 5 หากยังไม่ถูกกำจัด ฉันไม่ได้ดู x87 register-copy ( fld st(i)) แต่คุณถูกต้องสำหรับ Haswell / Broadwell: มันทำงานบน p01 Skylake รันบน p05, SnB รันบน p0, IvB รันบน p5 ดังนั้น IVB / SKL ทำบางสิ่ง x87 (รวมถึงการเปรียบเทียบ) ใน p5 แต่ SNB / HSW / BDW ไม่ใช้ p5 เลยสำหรับ x87
Peter Cordes

11

คุณสามารถใช้long doubleสำหรับการคำนวณ ใน x86 ควรเป็นรูปแบบ 80 บิต เฉพาะมรดก x87 FPU เท่านั้นที่รองรับสิ่งนี้

ข้อบกพร่องเล็กน้อยของ x87 FPU:

  1. การขาด SIMD อาจต้องการคำแนะนำเพิ่มเติม
  2. สแต็กเป็นปัญหาสำหรับซุปเปอร์สเกลาร์และสถาปัตยกรรมที่วางท่อ
  3. ชุดรีจิสเตอร์แยกต่างหากและค่อนข้างเล็กอาจต้องการการแปลงจากรีจิสเตอร์อื่นและการดำเนินการของหน่วยความจำเพิ่มเติม
  4. บน Core i7 มี 3 พอร์ตสำหรับ SSE และ 2 เท่านั้นสำหรับ x87 โปรเซสเซอร์สามารถประมวลผลคำสั่งแบบขนานได้น้อยลง

3
สำหรับคณิตศาสตร์สเกลาร์คำสั่งทางคณิตศาสตร์ x87 นั้นช้ากว่าปกติเล็กน้อยเท่านั้น การจัดเก็บ / การโหลดตัวถูกดำเนินการ 10byte นั้นช้าลงอย่างมากและการออกแบบแบบกองซ้อนของ x87 มีแนวโน้มที่จะต้องการคำแนะนำพิเศษ (เช่นfxch) ด้วย-ffast-mathคอมไพเลอร์ที่ดีอาจทำให้เวกเตอร์ของ monte-carlo loops และ x87 จะป้องกันสิ่งนั้น
Peter Cordes

ฉันได้ขยายคำตอบของฉันเล็กน้อย
Michas

1
Re: 4: คุณกำลังพูดถึง i7 รุ่นไหนและคำแนะนำใดบ้าง? Haswell สามารถทำงานmulssบน P01 แต่เฉพาะใน fmul ทำงานบนเช่นเดียวกับ มีเพียงสองพอร์ตการดำเนินการที่จัดการกับ ops คณิตศาสตร์ (ข้อยกเว้นเพียงอย่างเดียวคือ Skylake ลดหน่วยเพิ่มพิเศษและทำงานในหน่วย FMA ใน p01 แต่ใน p5 ดังนั้นโดยการผสมในคำแนะนำบางอย่างพร้อมกันคุณในทางทฤษฎีสามารถทำ FLOP / s ได้ทั้งหมดเล็กน้อย)p0addssp1faddaddssfaddfaddfma...ps
Peter Cordes

2
นอกจากนี้ทราบว่าของ Windows x86-64 ABI มี 64bit คือมันยังคงเป็นเพียงแค่long double doubleSysV ABI ไม่ใช้ 80bit long doubleแม้ว่า นอกจากนี้: 2: การเปลี่ยนชื่อรีจิสเตอร์จะเปิดเผยความเท่าเทียมใน stack register สถาปัตยกรรมแบบอิงกองซ้อนต้องการคำแนะนำพิเศษบางอย่างเช่นfxchgesp เมื่อทำการคำนวณแบบขนาน interleaving ดังนั้นมันจึงเหมือนเป็นการยากที่จะแสดงความขนานโดยไม่มีหน่วยความจำแบบไปกลับแทนที่จะยากที่ uarch จะใช้ประโยชน์จากสิ่งที่มีอยู่ คุณไม่จำเป็นต้องมีการแปลงเพิ่มเติมจาก regs อื่น ๆ ไม่แน่ใจว่าคุณหมายถึงอะไร
Peter Cordes

6

ตอบกลับล่าช้า แต่ฉันไม่รู้สึกว่าเราใช้รายการที่ลิงก์ไปแล้วและ TLB ที่ไม่เหมาะสม

ใช้ mmap เพื่อจัดสรรโหนดของคุณซึ่งส่วนใหญ่ใช้ MSB ของที่อยู่ สิ่งนี้จะส่งผลให้กลุ่มการค้นหา TLB มีความยาวหน้าคือ 12 บิตปล่อยให้ 52 บิตสำหรับการแปลหรือประมาณ 5 ระดับที่ต้องทำการสำรวจแต่ละครั้ง ด้วยโชคเล็กน้อยที่พวกเขาต้องไปที่หน่วยความจำทุกครั้งสำหรับการค้นหา 5 ระดับพร้อมการเข้าถึงหน่วยความจำ 1 ครั้งเพื่อไปยังโหนดของคุณระดับบนสุดน่าจะอยู่ในแคชอยู่ที่ไหนสักแห่งดังนั้นเราจึงหวังว่าการเข้าถึงหน่วยความจำ 5 * วางโหนดเพื่อให้เป็นเส้นขอบที่แย่ที่สุดเพื่อให้การอ่านตัวชี้ถัดไปอาจทำให้การค้นหาคำแปลอีก 3-4 ครั้ง นี่อาจทำให้แคชทั้งหมดเสียหายเนื่องจากการค้นหาคำแปลจำนวนมาก ด้วยขนาดของตารางเสมือนอาจทำให้ข้อมูลผู้ใช้ส่วนใหญ่ถูกเพจเป็นดิสก์สำหรับเวลาเพิ่มเติม

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


ตารางหน้า x86-64 มีความลึก 4 ระดับสำหรับที่อยู่เสมือน 48 บิต (PTE มีที่อยู่จริง 52 บิต) ซีพียูในอนาคตจะสนับสนุนคุณสมบัติตารางเพจ 5 ระดับสำหรับพื้นที่แอดเดรสเสมือนอีก 9 บิต (57) เหตุใดใน 64 บิตที่อยู่เสมือนจึงสั้น 4 บิต (48 บิตยาว) เมื่อเทียบกับที่อยู่จริง (ยาว 52 บิต) . ระบบปฏิบัติการจะไม่เปิดใช้งานตามค่าเริ่มต้นเพราะจะช้ากว่าและไม่ก่อให้เกิดประโยชน์เว้นแต่คุณจะต้องการพื้นที่ที่อยู่ที่เหมาะสม
ปีเตอร์

แต่ใช่ความคิดที่สนุก คุณอาจจะใช้mmapไฟล์หรือพื้นที่หน่วยความจำที่แชร์เพื่อรับหลาย ๆ ที่อยู่เสมือนสำหรับหน้าฟิสิคัลเดียวกัน (ที่มีเนื้อหาเดียวกัน) ซึ่งทำให้ TLB คิดถึงมากกว่าจำนวนฟิสิคัลแรมที่เท่ากัน หากรายการที่เชื่อมโยงของคุณnextเป็นเพียงออฟเซ็ตสัมพัทธ์คุณอาจมีการแมปชุดของหน้าเดียวกันพร้อมกับ+4096 * 1024จนกว่าคุณจะได้เข้าสู่หน้าทางกายภาพที่แตกต่าง หรือแน่นอนครอบคลุมหลายหน้าเพื่อหลีกเลี่ยง L1d แคชนิยม มีการแคช PDE ระดับสูงกว่าในฮาร์ดแวร์หน้าการเดินดังนั้นใช่กระจายออกไปในพื้นที่ addr คุณธรรม!
ปีเตอร์

การเพิ่มออฟเซตไปยังที่อยู่เก่ายังทำให้เวลาในการโหลดช้าลงด้วยการเอาชนะ [กรณีพิเศษสำหรับ[reg+small_offset]โหมดการกำหนดแอดเดรส] ( มีการลงโทษเมื่อฐาน + ออฟเซ็ตอยู่ในหน้าอื่นที่ไม่ใช่ฐานหรือไม่ ); คุณอาจจะได้รับความทรงจำแหล่งที่มาaddของ 64 [reg+reg]บิตชดเชยหรือคุณจะได้รับการโหลดและการจัดทำดัชนีโหมดเช่น ดูเพิ่มเติมจะเกิดอะไรขึ้นหลังจาก L2 TLB พลาด - การเดินหน้าดึงข้อมูลผ่านแคช L1d ใน SnB-family
ปีเตอร์คอร์เดส
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.