Tối ưu hóa một chương trình cho đường ống trong CPU gia đình Intel Sandybridge


322

Tôi đã làm rối trí não trong một tuần để cố gắng hoàn thành nhiệm vụ này và tôi hy vọng ai đó ở đây có thể đưa tôi đến con đường đúng đắn. Hãy để tôi bắt đầu với hướng dẫn của người hướng dẫn:

Nhiệm vụ của bạn trái ngược với nhiệm vụ trong phòng thí nghiệm đầu tiên của chúng tôi, đó là tối ưu hóa chương trình số nguyên tố. Mục đích của bạn trong nhiệm vụ này là để bi quan hóa chương trình, tức là làm cho nó chạy chậm hơn. Cả hai đều là chương trình sử dụng nhiều CPU. Họ mất vài giây để chạy trên PC phòng thí nghiệm của chúng tôi. Bạn không thể thay đổi thuật toán.

Để mở rộng chương trình, hãy sử dụng kiến ​​thức của bạn về cách thức hoạt động của đường ống Intel i7. Hãy tưởng tượng các cách để sắp xếp lại các đường dẫn chỉ dẫn để giới thiệu WAR, RAW và các mối nguy hiểm khác. Hãy nghĩ cách để giảm thiểu hiệu quả của bộ đệm. Không đủ năng lực.

Nhiệm vụ đã đưa ra lựa chọn các chương trình Whetstone hoặc Monte-Carlo. Các ý kiến ​​về hiệu quả bộ đệm hầu hết chỉ áp dụng cho Whetstone, nhưng tôi đã chọn chương trình mô phỏng 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;
}

Những thay đổi tôi đã thực hiện dường như tăng thời gian chạy mã lên một giây nhưng tôi không hoàn toàn chắc chắn những gì tôi có thể thay đổi để trì hoãn đường ống mà không cần thêm mã. Một điểm đến đúng hướng sẽ là tuyệt vời, tôi đánh giá cao bất kỳ câu trả lời.


Cập nhật: giáo sư đã giao nhiệm vụ này đã đăng một số chi tiết

Những điểm nổi bật là:

  • Đó là lớp kiến ​​trúc học kỳ thứ hai tại một trường cao đẳng cộng đồng (sử dụng sách giáo khoa Hennessy và Patterson).
  • các máy tính trong phòng thí nghiệm có CPU Haswell
  • Các sinh viên đã được tiếp xúc với CPUIDhướng dẫn và cách xác định kích thước bộ đệm, cũng như nội tại và CLFLUSHhướng dẫn.
  • bất kỳ tùy chọn trình biên dịch đều được cho phép, và asm nội tuyến cũng vậy.
  • Viết thuật toán căn bậc hai của riêng bạn đã được công bố là bên ngoài nhạt

Nhận xét của Cowmoogun về luồng meta cho thấy rằng việc tối ưu hóa trình biên dịch không rõ ràng có thể là một phần của điều này, và giả định-O0 , và thời gian chạy tăng 17% là hợp lý.

Vì vậy, có vẻ như mục tiêu của bài tập là khiến sinh viên sắp xếp lại công việc hiện có để giảm sự song song ở cấp độ hướng dẫn hoặc những thứ tương tự, nhưng đó không phải là điều xấu khi mọi người đào sâu và học hỏi nhiều hơn.


Hãy nhớ rằng đây là một câu hỏi về kiến ​​trúc máy tính, không phải là một câu hỏi về cách làm cho C ++ chậm nói chung.


97
Tôi nghe nói i7 làm rất kém vớiwhile(true){}
Cliff AB


5
Với openmp nếu bạn làm điều đó không tốt, bạn phải có thể làm cho các luồng N mất nhiều thời gian hơn 1.
Flexo

9
Câu hỏi này hiện đang được thảo luận trong meta
Madara's Ghost

3
@bluefeet: Tôi đã thêm rằng vì nó đã thu hút được một phiếu bầu gần trong một giờ sau khi được mở lại. Chỉ cần 5 người đi cùng và VTC mà không nhận ra việc đọc các bình luận để xem nó đang được thảo luận về meta. Bây giờ có một cuộc bỏ phiếu chặt chẽ khác. Tôi nghĩ rằng ít nhất một câu sẽ giúp tránh chu kỳ đóng / mở lại.
Peter Cordes

Câu trả lời:


405

Đọc nền quan trọng: Microner pdf của Agner Fog , và có lẽ cũng là Ulrich Drepper's Điều mà mọi lập trình viên nên biết về bộ nhớ . Xem thêm các liên kết khác trongthẻ wiki, đặc biệt là các hướng dẫn tối ưu hóa của Intel và phân tích của David Kanter về vi kiến ​​trúc Haswell, với các sơ đồ .

Nhiệm vụ rất tuyệt vời; tốt hơn nhiều so với những gì tôi đã thấy nơi sinh viên được yêu cầu tối ưu hóa một số mãgcc -O0 , học một loạt các thủ thuật không quan trọng trong mã thực. Trong trường hợp này, bạn được yêu cầu tìm hiểu về đường ống CPU và sử dụng điều đó để hướng dẫn các nỗ lực tối ưu hóa của bạn, không chỉ là đoán mò. Phần thú vị nhất của phần này là biện minh cho mỗi sự bi quan với "sự bất tài của bệnh nhân", không phải là ác ý có chủ ý.


Các vấn đề với từ ngữ và mã bài tập :

Các tùy chọn dành riêng cho uarch cho mã này bị giới hạn. Nó không sử dụng bất kỳ mảng nào và phần lớn chi phí là các lệnh gọi đến exp/ các loghàm thư viện. Không có một cách rõ ràng để có ít nhiều song song mức độ hướng dẫn và chuỗi phụ thuộc mang theo vòng lặp là rất ngắn.

Tôi rất muốn thấy một câu trả lời đã cố gắng làm chậm lại từ việc sắp xếp lại các biểu thức để thay đổi các phụ thuộc, để giảm ILP chỉ từ các phụ thuộc (mối nguy). Tôi đã không thử nó.

Các CPU gia đình Intel Sandybridge là các thiết kế không theo thứ tự tích cực, sử dụng nhiều bóng bán dẫn và năng lượng để tìm song song và tránh các mối nguy hiểm (phụ thuộc) sẽ gây rắc rối cho đường ống theo thứ tự RISC cổ điển . Thông thường, các mối nguy hiểm truyền thống duy nhất làm chậm nó là các phụ thuộc RAW "thật" khiến thông lượng bị giới hạn bởi độ trễ.

Nguy cơ WAR và WAW cho các thanh ghi khá nhiều không phải là vấn đề, nhờ đăng ký đổi tên . (ngoại trừpopcnt/lzcnt/tzcnt, có điểm phụ thuộc sai vào đích của CPU Intel , mặc dù nó chỉ ghi. Ví dụ, WAW bị xử lý dưới dạng nguy hiểm RAW + ghi). Để đặt hàng bộ nhớ, các CPU hiện đại sử dụng hàng đợi lưu trữ để trì hoãn cam kết vào bộ đệm cho đến khi nghỉ hưu, cũng tránh các nguy cơ WAR và WAW .

Tại sao mulss chỉ mất 3 chu kỳ trên Haswell, khác với bảng hướng dẫn của Agner? có thêm thông tin về việc đổi tên đăng ký và ẩn độ trễ FMA trong vòng lặp sản phẩm chấm FP.


Tên thương hiệu "i7" được giới thiệu với Nehalem (kế thừa của Core2) và một số hướng dẫn sử dụng của Intel thậm chí còn nói "Core i7" khi chúng có nghĩa là Nehalem, nhưng họ vẫn giữ thương hiệu "i7" cho Sandybridge và sau đó là các kiến ​​trúc vi mô. SnB là khi gia đình P6 phát triển thành một loài mới, họ SnB . Theo nhiều cách, Nehalem có nhiều điểm tương đồng với Pentium III hơn với Sandybridge (ví dụ: quầy đăng ký đọc và quầy đọc ROB không xảy ra trên SnB, vì nó đã thay đổi thành sử dụng tệp đăng ký vật lý. Ngoài ra, bộ đệm uop và nội bộ khác định dạng uop). Thuật ngữ "kiến trúc i7" không hữu ích, bởi vì rất ít ý nghĩa khi nhóm gia đình SnB với Nehalem chứ không phải Core2. (Tuy nhiên, Nehalem đã giới thiệu kiến ​​trúc bộ đệm L3 bao gồm để kết nối nhiều lõi với nhau. Và cả GPU tích hợp. Vì vậy, ở cấp độ chip, việc đặt tên có ý nghĩa hơn.)


Tóm tắt những ý tưởng hay mà sự bất tài của ma quỷ có thể biện minh

Ngay cả những người không có năng lực tiểu đường cũng không có khả năng thêm công việc rõ ràng vô dụng hoặc một vòng lặp vô hạn, và làm cho một mớ hỗn độn với các lớp C ++ / Boost nằm ngoài phạm vi của nhiệm vụ.

  • Đa luồng với một bộ đếm vòng lặp chia sẻ duy nhất std::atomic<uint64_t>, do đó tổng số lần lặp đúng xảy ra. Nguyên tử uint64_t đặc biệt xấu với -m32 -march=i586. Đối với các điểm thưởng, hãy sắp xếp để nó được căn chỉnh sai và vượt qua một ranh giới trang với sự phân chia không đồng đều (không phải 4: 4).
  • Chia sẻ sai cho một số biến không nguyên tử khác -> xóa đường ống suy đoán theo thứ tự bộ nhớ, cũng như bỏ lỡ bộ nhớ cache.
  • Thay vì sử dụng -trên các biến FP, XOR byte cao với 0x80 để lật bit dấu, gây ra các quầy chuyển tiếp cửa hàng .
  • Thời gian mỗi lần lặp độc lập, với một cái gì đó thậm chí còn nặng hơn RDTSC. ví dụ CPUID/ RDTSChoặc một hàm thời gian thực hiện cuộc gọi hệ thống. Hướng dẫn nối tiếp vốn là đường ống không thân thiện.
  • Thay đổi nhân với hằng số để chia cho đối ứng của chúng ("để dễ đọc"). div là chậm và không đầy đủ đường ống.
  • Vector hóa số nhân / sqrt với AVX (SIMD), nhưng không sử dụng vzerouppertrước các lệnh gọi đến thư viện toán học vô hướng exp()và các log()hàm, gây ra các quầy chuyển tiếp AVX <-> SSE .
  • Lưu trữ đầu ra RNG trong một danh sách được liên kết hoặc trong các mảng mà bạn đi qua trật tự. Tương tự cho kết quả của mỗi lần lặp và tổng ở cuối.

Cũng được nêu trong câu trả lời này nhưng được loại trừ khỏi bản tóm tắt: các đề xuất sẽ chậm như trên CPU không có đường ống hoặc dường như không thể chứng minh được ngay cả với sự bất tài của bệnh nhân tiểu đường. ví dụ: nhiều ý tưởng gimp-the-trình biên dịch tạo ra asm rõ ràng khác nhau / tệ hơn.


Đa luồng xấu

Có thể sử dụng OpenMP cho các vòng lặp đa luồng với rất ít lần lặp, với nhiều chi phí hơn tốc độ đạt được. Mã monte-carlo của bạn có đủ song song để thực sự tăng tốc, đặc biệt, đặc biệt. nếu chúng ta thành công trong việc làm cho mỗi lần lặp chậm. (Mỗi luồng tính một phần payoff_sum, được thêm vào cuối). #omp paralleltrên vòng lặp đó có lẽ sẽ là một tối ưu hóa, không phải là bi quan.

Đa luồng nhưng buộc cả hai luồng chia sẻ cùng một bộ đếm vòng lặp (với số atomicgia tăng nên tổng số lần lặp là chính xác). Điều này có vẻ hợp lý về mặt tiểu đường. Điều này có nghĩa là sử dụng một staticbiến như một bộ đếm vòng lặp. Điều này biện minh cho việc sử dụng các atomicbộ đếm vòng lặp và tạo ra ping-ponging dòng bộ đệm thực tế (miễn là các luồng không chạy trên cùng một lõi vật lý với siêu phân luồng; có thể không chậm như vậy ). Dù sao, đây là nhiều chậm hơn so với trường hợp bỏ tranh cho lock inc. Và lock cmpxchg8bđể tăng nguyên tử, một uint64_thệ thống 32bit sẽ phải thử lại trong một vòng lặp thay vì phần cứng phân xử một nguyên tử inc.

Đồng thời tạo chia sẻ sai , trong đó nhiều luồng giữ dữ liệu riêng tư của họ (ví dụ trạng thái RNG) trong các byte khác nhau của cùng một dòng bộ đệm. (Hướng dẫn của Intel về nó, bao gồm các bộ đếm hoàn hảo để xem xét) . Có một khía cạnh cụ thể về kiến ​​trúc vi mô đối với vấn đề này : CPU Intel suy đoán việc sắp xếp sai bộ nhớ không xảy ra và có một sự kiện hoàn hảo rõ ràng theo thứ tự bộ nhớ để phát hiện điều này, ít nhất là trên P4 . Hình phạt có thể không lớn bằng Haswell. Khi liên kết đó chỉ ra,lock hướng dẫn ed giả định điều này sẽ xảy ra, tránh suy đoán sai. Một tải bình thường suy đoán rằng các lõi khác sẽ không làm mất hiệu lực một dòng bộ đệm giữa khi tải thực thi và khi nó nghỉ theo thứ tự chương trình (trừ khi bạn sử dụngpause ). Chia sẻ đúng mà không có lockhướng dẫn ed thường là một lỗi. Sẽ rất thú vị khi so sánh một bộ đếm vòng lặp không nguyên tử với trường hợp nguyên tử. Để thực sự bi quan, hãy giữ bộ đếm vòng lặp nguyên tử được chia sẻ và gây ra chia sẻ sai trong cùng hoặc một dòng bộ đệm khác cho một số biến khác.


Ý tưởng cụ thể của uarch:

Nếu bạn có thể giới thiệu bất kỳ chi nhánh không thể đoán trước , điều đó sẽ làm giảm đáng kể mã. Các CPU x86 hiện đại có các đường ống khá dài, do đó, một dự đoán sai có giá ~ 15 chu kỳ (khi chạy từ bộ đệm uop).


Chuỗi phụ thuộc:

Tôi nghĩ rằng đây là một trong những phần dự định của bài tập.

Đánh bại khả năng của CPU để khai thác song song mức hướng dẫn bằng cách chọn một thứ tự các hoạt động có một chuỗi phụ thuộc dài thay vì nhiều chuỗi phụ thuộc ngắn. Trình biên dịch không được phép thay đổi thứ tự các thao tác cho các tính toán FP trừ khi bạn sử dụng -ffast-math, vì điều đó có thể thay đổi kết quả (như được thảo luận dưới đây).

Để thực sự làm điều này hiệu quả, hãy tăng độ dài của chuỗi phụ thuộc mang theo vòng lặp. Không có gì nhảy vọt là hiển nhiên, mặc dù: Các vòng lặp như được viết có chuỗi phụ thuộc vòng lặp rất ngắn: chỉ là một bổ sung FP. (3 chu kỳ). Nhiều lần lặp có thể có các phép tính của chúng trong chuyến bay cùng một lúc, bởi vì chúng có thể bắt đầu tốt trước khi payoff_sum +=kết thúc lần lặp trước. ( log()expthực hiện nhiều hướng dẫn, nhưng không nhiều hơn cửa sổ không theo thứ tự của Haswell để tìm song song: kích thước ROB = 192 uops miền hợp nhất và kích thước lịch trình = 60 uops không sử dụng tên miền. Ngay khi việc thực hiện lặp lại hiện tại tiến triển đủ xa để nhường chỗ cho các hướng dẫn từ lần lặp tiếp theo phát hành, bất kỳ phần nào của nó đã sẵn sàng (ví dụ chuỗi dep độc lập / riêng biệt) có thể bắt đầu thực thi khi các lệnh cũ hơn rời khỏi các đơn vị thực thi miễn phí (ví dụ vì chúng bị tắc nghẽn về độ trễ, không phải thông lượng.).

Trạng thái RNG gần như chắc chắn sẽ là một chuỗi phụ thuộc mang vòng lặp dài hơn so với addps.


Sử dụng các hoạt động chậm hơn / nhiều FP hơn (đặc biệt là phân chia nhiều hơn):

Chia cho 2.0 thay vì nhân 0,5, v.v. Hệ số nhân FP được thiết kế rất nhiều trong các thiết kế của Intel và có thông lượng trên 0,5c trên Haswell và sau đó. FP divsd/ divpdchỉ là một phần đường ống . (Mặc dù Skylake có mức thông lượng ấn tượng trên mỗi 4c cho divpd xmm, với độ trễ 13-14c, so với không có đường ống nào trên Nehalem (7-22c)).

Các do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);đang thử nghiệm rõ ràng cho một khoảng cách, vì vậy rõ ràng nó sẽ là thích hợp để sqrt()nó. : P ( sqrtthậm chí chậm hơn div).

Như @Paul Clayton đề xuất, viết lại các biểu thức với các tương đương liên kết / phân phối có thể giới thiệu nhiều công việc hơn (miễn là bạn không sử dụng -ffast-mathđể cho phép trình biên dịch tối ưu hóa lại). (exp(T*(r-0.5*v*v))có thể trở thành exp(T*r - T*v*v/2.0). Lưu ý rằng mặc dù toán học trên các số thực là kết hợp, toán học dấu phẩy động là không , ngay cả khi không xem xét tràn / NaN (đó là lý do tại sao -ffast-mathkhông bật theo mặc định). Xem bình luận của Paul cho một pow()đề nghị lồng nhau rất nhiều lông .

Nếu bạn có thể thu nhỏ các phép tính xuống các số rất nhỏ, thì các toán học FP mất ~ 120 chu kỳ bổ sung để bẫy thành vi mã khi một phép toán trên hai số bình thường tạo ra một số không bình thường . Xem microarch pdf của Agner Fog để biết con số và chi tiết chính xác. Điều này là không thể vì bạn có rất nhiều bội số, vì vậy hệ số tỷ lệ sẽ được bình phương và nằm dưới mức 0,0. Tôi không thấy bất kỳ cách nào để biện minh cho việc mở rộng quy mô cần thiết với sự bất tài (thậm chí là độc ác), chỉ có ác ý cố ý.


Nếu bạn có thể sử dụng nội tại ( <immintrin.h>)

Sử dụng movntiđể đuổi dữ liệu của bạn khỏi bộ nhớ cache . Diabolical: nó mới và được sắp xếp yếu, vì vậy nên để CPU chạy nhanh hơn, phải không? Hoặc xem câu hỏi được liên kết đó cho trường hợp ai đó gặp nguy hiểm khi thực hiện chính xác điều này (đối với bài viết rải rác chỉ có một số vị trí nóng). clflushcó lẽ là không thể nếu không có ác ý.

Sử dụng xáo trộn số nguyên giữa các phép toán FP để gây ra độ trễ bỏ qua.

Trộn các hướng dẫn SSE và AVX mà không sử dụng đúng cách vzerouppergây ra các quầy hàng lớn trong Skylake trước (và một hình phạt khác trong Skylake ). Ngay cả khi không có điều đó, vectơ xấu có thể tệ hơn vô hướng (nhiều chu kỳ sử dụng dữ liệu xáo trộn vào / ra vectơ hơn được lưu bằng cách thực hiện các thao tác add / sub / mul / div / sqrt cho 4 lần lặp Monte-Carlo cùng một lúc, với vectơ 256b) . các đơn vị thực thi add / sub / mul được cung cấp đầy đủ theo chiều rộng và toàn chiều rộng, nhưng div và sqrt trên các vectơ 256b không nhanh như trên các vectơ 128b (hoặc vô hướng), vì vậy việc tăng tốc không đáng kểdouble.

exp()log()không có hỗ trợ phần cứng, do đó, phần đó sẽ yêu cầu trích xuất các phần tử vectơ trở lại vô hướng và gọi hàm thư viện một cách riêng biệt, sau đó xáo trộn các kết quả trở lại thành một vectơ. libm thường được biên dịch để chỉ sử dụng SSE2, do đó sẽ sử dụng các bảng mã SSE kế thừa của các hướng dẫn toán học vô hướng. Nếu mã của bạn sử dụng các vectơ 256b và các cuộc gọi expmà không thực hiện vzerouppertrước, thì bạn bị đình trệ. Sau khi trở về, một lệnh AVX-128 muốn vmovsdthiết lập phần tử vectơ tiếp theo làm đối số expcũng sẽ bị đình trệ. Và sau đó exp()sẽ bị đình trệ một lần nữa khi nó chạy một lệnh SSE. Đây chính xác là những gì đã xảy ra trong câu hỏi này , gây ra sự chậm lại 10 lần. (Cảm ơn @ZBoson).

Xem thêm các thí nghiệm của Nathan Kurz với lib toán học của Intel so với glibc cho mã này . Glibc trong tương lai sẽ đi kèm với việc triển khai véc tơ exp()và vv.


Nếu nhắm mục tiêu trước IvB, hoặc đặc biệt. Nehalem, hãy thử lấy gcc để tạo các quầy đăng ký một phần với các hoạt động 16 bit hoặc 8 bit, sau đó là các hoạt động 32 bit hoặc 64 bit. Trong hầu hết các trường hợp, gcc sẽ sử dụng movzxsau thao tác 8 hoặc 16 bit, nhưng đây là trường hợp gcc sửa đổi ahvà sau đó đọcax


Với asm (nội tuyến):

Với asm (nội tuyến), bạn có thể phá vỡ bộ đệm uop: Một đoạn mã 32B không phù hợp với ba dòng bộ đệm 6uop buộc chuyển từ bộ đệm uop sang bộ giải mã. Một người không đủ năng lực ALIGNsử dụng nhiều byte đơn nopthay vì một vài nopgiây dài trên một mục tiêu nhánh bên trong vòng lặp bên trong có thể thực hiện thủ thuật. Hoặc đặt phần đệm căn chỉnh sau nhãn, thay vì trước. : P Điều này chỉ quan trọng nếu frontend là một nút cổ chai, điều này sẽ không xảy ra nếu chúng ta thành công trong việc bi quan hóa phần còn lại của mã.

Sử dụng mã tự sửa đổi để kích hoạt xóa đường ống (còn gọi là máy móc).

Các quầy LCP từ các hướng dẫn 16 bit với số lượng quá lớn để phù hợp với 8 bit dường như không hữu ích. Bộ đệm uop trên SnB và sau đó có nghĩa là bạn chỉ phải trả tiền phạt giải mã một lần. Trên Nehalem (i7 đầu tiên), nó có thể hoạt động cho một vòng lặp không phù hợp với bộ đệm vòng lặp 28 uop. gcc đôi khi sẽ tạo ra các hướng dẫn như vậy, ngay cả với -mtune=intelvà khi nó có thể đã sử dụng một lệnh 32 bit.


Một thành ngữ phổ biến cho thời gian là CPUID(để tuần tự hóa) sau đóRDTSC . Thời gian mỗi lần lặp riêng biệt với một CPUID/ RDTSCđể đảm bảo rằng RDTSCkhông được sắp xếp lại theo các hướng dẫn trước đó, điều này sẽ làm mọi thứ chậm lại rất nhiều . (Trong cuộc sống thực, cách thông minh theo thời gian là tính thời gian tất cả các lần lặp lại với nhau, thay vì định thời gian riêng biệt và thêm chúng lên).


Gây ra nhiều lỗi bộ nhớ cache và chậm bộ nhớ khác

Sử dụng một union { double d; char a[8]; }cho một số biến của bạn. Gây ra một gian hàng chuyển tiếp cửa hàng bằng cách thực hiện một cửa hàng hẹp (hoặc Đọc-Sửa đổi-Viết) thành một trong các byte. (Bài viết wiki đó cũng đề cập đến rất nhiều công cụ vi kiến ​​trúc khác cho hàng đợi tải / lưu trữ). ví dụ: lật dấu hiệu của doubleXOR 0x80 chỉ bằng byte cao , thay vì -toán tử. Nhà phát triển không đủ năng lực tiểu đường có thể đã nghe nói rằng FP chậm hơn số nguyên, và do đó cố gắng làm càng nhiều càng tốt bằng cách sử dụng số nguyên op. (Một trình biên dịch rất tốt nhắm mục tiêu toán học FP trong các thanh ghi SSE có thể có thể biên dịch nó thành mộtxorps với một hằng số trong một thanh ghi xmm khác, nhưng cách duy nhất không tệ cho x87 là nếu trình biên dịch nhận ra rằng nó phủ định giá trị và thay thế phép cộng tiếp theo bằng phép trừ.)


Sử dụng volatilenếu bạn đang biên dịch -O3và không sử dụng std::atomic, để buộc trình biên dịch thực sự lưu trữ / tải lại mọi nơi. Các biến toàn cục (thay vì cục bộ) cũng sẽ buộc một số cửa hàng / tải lại, nhưng thứ tự yếu của mô hình bộ nhớ C ++ không yêu cầu trình biên dịch phải đổ / tải lại vào bộ nhớ mọi lúc.

Thay thế các vars cục bộ bằng các thành viên của một cấu trúc lớn, để bạn có thể kiểm soát bố cục bộ nhớ.

Sử dụng các mảng trong cấu trúc để đệm (và lưu trữ các số ngẫu nhiên, để chứng minh sự tồn tại của chúng).

Chọn bố cục bộ nhớ của bạn để mọi thứ đi vào một dòng khác trong cùng một "bộ" trong bộ đệm L1 . Đó chỉ là liên kết 8 chiều, tức là mỗi bộ có 8 "cách". Dòng cache là 64B.

Thậm chí tốt hơn, đặt mọi thứ cách nhau chính xác 4096B, vì các tải có sự phụ thuộc sai vào các cửa hàng vào các trang khác nhau nhưng có cùng độ lệch trong một trang . Các CPU không theo thứ tự tích cực sử dụng Định hướng bộ nhớ để tìm ra khi nào tải và lưu trữ có thể được sắp xếp lại mà không thay đổi kết quả và việc triển khai của Intel có những điểm sai khiến ngăn tải bắt đầu sớm. Có lẽ họ chỉ kiểm tra các bit bên dưới phần bù trang, vì vậy kiểm tra có thể bắt đầu trước khi TLB đã dịch các bit cao từ một trang ảo sang một trang vật lý. Cũng như hướng dẫn của Agner, hãy xem câu trả lời từ Stephen Canon , và cũng là phần gần cuối câu trả lời của @Krazy Glew cho cùng một câu hỏi. (Andy Glew là một trong những kiến ​​trúc sư của kiến ​​trúc vi mô P6 ban đầu của Intel.)

Sử dụng __attribute__((packed))để cho phép bạn căn chỉnh sai các biến để chúng vượt qua ranh giới dòng bộ đệm hoặc thậm chí trang. (Vì vậy, một tải của một người doublecần dữ liệu từ hai dòng bộ đệm). Tải không đúng sẽ không bị phạt trong bất kỳ Intel i7 uarch nào, ngoại trừ khi vượt qua các dòng bộ đệm và dòng trang. Chia tách dòng bộ đệm vẫn mất thêm chu kỳ . Skylake giảm đáng kể hình phạt cho tải phân chia trang, từ 100 đến 5 chu kỳ. (Mục 2.1.3) . Có lẽ liên quan đến việc có thể thực hiện song song hai trang.

Việc chia trang trên atomic<uint64_t>chỉ là về trường hợp xấu nhất , đặc biệt. nếu đó là 5 byte trong một trang và 3 byte ở trang kia hoặc bất cứ thứ gì khác ngoài 4: 4. Ngay cả việc phân tách xuống giữa cũng hiệu quả hơn cho việc phân chia dòng bộ đệm với các vectơ 16B trên một số uarches, IIRC. Đặt mọi thứ vào một alignas(4096) struct __attribute((packed))(để tiết kiệm không gian, tất nhiên), bao gồm một mảng để lưu trữ cho kết quả RNG. Đạt được sự sai lệch bằng cách sử dụng uint8_thoặc uint16_tcho một cái gì đó trước quầy.

Nếu bạn có thể có được trình biên dịch để sử dụng các chế độ địa chỉ được lập chỉ mục, điều đó sẽ đánh bại phản ứng tổng hợp vi mô . Có thể bằng cách sử dụng #defines để thay thế các biến vô hướng đơn giản bằng my_data[constant].

Nếu bạn có thể giới thiệu thêm một mức độ gián tiếp, do đó, địa chỉ tải / lưu trữ không được biết sớm, điều đó có thể làm tăng thêm.


Các mảng ngang theo thứ tự không liền kề

Tôi nghĩ rằng chúng ta có thể đưa ra lời biện minh bất tài cho việc giới thiệu một mảng ở vị trí đầu tiên: Nó cho phép chúng ta tách việc tạo số ngẫu nhiên khỏi việc sử dụng số ngẫu nhiên. Kết quả của mỗi lần lặp cũng có thể được lưu trữ trong một mảng, để được tóm tắt sau (với sự bất tài về bệnh tiểu đường nhiều hơn).

Đối với "tính ngẫu nhiên tối đa", chúng ta có thể có một chuỗi lặp trên mảng ngẫu nhiên ghi các số ngẫu nhiên mới vào đó. Chuỗi tiêu thụ các số ngẫu nhiên có thể tạo ra một chỉ mục ngẫu nhiên để tải một số ngẫu nhiên từ đó. . xóa đường ống dẫn cụ thể (như đã thảo luận trước đó cho trường hợp chia sẻ sai).

Để bi quan hóa tối đa, hãy lặp qua mảng của bạn với một bước dài 4096 byte (tức là 512 nhân đôi). ví dụ

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

Vì vậy, mẫu truy cập là 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 8208, ...

Đây là những gì bạn nhận được khi truy cập vào một mảng 2D như double rng_array[MAX_ROWS][512]sai thứ tự (lặp qua các hàng, thay vì các cột trong một hàng trong vòng lặp bên trong, như được đề xuất bởi @JesperJuhl). Nếu sự không đủ năng lực của ma quỷ có thể biện minh cho một mảng 2D có kích thước như thế, thì sự bất tài trong thế giới thực của khu vườn dễ dàng biện minh cho việc lặp với mô hình truy cập sai. Điều này xảy ra trong mã thực trong cuộc sống thực.

Điều chỉnh giới hạn vòng lặp nếu cần thiết để sử dụng nhiều trang khác nhau thay vì sử dụng lại cùng một vài trang, nếu mảng đó không lớn. Tìm nạp trước phần cứng không hoạt động (cũng như / tất cả) trên các trang. Trình tải trước có thể theo dõi một luồng tiến và một luồng lùi trong mỗi trang (đó là những gì xảy ra ở đây), nhưng sẽ chỉ hành động trên nó nếu băng thông bộ nhớ chưa bão hòa với không tìm nạp trước.

Điều này cũng sẽ tạo ra rất nhiều lỗi TLB, trừ khi các trang được hợp nhất thành một hugepage ( Linux thực hiện điều này một cách cơ hội để phân bổ ẩn danh (không được hỗ trợ tệp) như malloc/ newsử dụngmmap(MAP_ANONYMOUS) ).

Thay vì một mảng để lưu trữ danh sách kết quả, bạn có thể sử dụng danh sách được liên kết . Sau đó, mỗi lần lặp sẽ yêu cầu tải đuổi theo con trỏ (nguy cơ phụ thuộc thực sự RAW cho địa chỉ tải của tải tiếp theo). Với một cấp phát xấu, bạn có thể quản lý để phân tán các nút danh sách xung quanh trong bộ nhớ, đánh bại bộ đệm. Với một công cụ cấp phát không đủ năng lực, nó có thể đặt mọi nút ở đầu trang của chính nó. (ví dụ: phân bổ mmap(MAP_ANONYMOUS)trực tiếp, không chia nhỏ các trang hoặc theo dõi kích thước đối tượng để hỗ trợ chính xác free).


Chúng không thực sự đặc trưng cho kiến ​​trúc vi mô và ít liên quan đến đường ống (hầu hết trong số này cũng sẽ là một sự chậm lại trên CPU không có đường ống).

Hơi lạc đề: làm cho trình biên dịch tạo mã tệ hơn / làm nhiều việc hơn:

Sử dụng C ++ 11 std::atomic<int>std::atomic<double>cho mã pessimal nhất. Các lockhướng dẫn và các hướng dẫn ed khá chậm ngay cả khi không có sự tranh chấp từ một luồng khác.

-m32sẽ làm cho mã chậm hơn, vì mã x87 sẽ tệ hơn mã SSE2. Quy ước gọi 32 bit dựa trên ngăn xếp có nhiều hướng dẫn hơn và chuyển ngay cả các đối số FP trên ngăn xếp sang các hàm như thế nào exp(). atomic<uint64_t>::operator++trên -m32yêu cầu một lock cmpxchg8Bvòng lặp (i586). (Vì vậy, sử dụng nó cho bộ đếm vòng lặp! [Cười xấu xa]).

-march=i386cũng sẽ bi quan (cảm ơn @Jesper). FP so sánh với fcomchậm hơn 686 fcomi. Pre-586 không cung cấp một kho lưu trữ 64 bit nguyên tử, (nói riêng là cmpxchg), vì vậy tất cả các atomicop 64bit đều biên dịch thành các lệnh gọi hàm libgcc (có thể được biên dịch cho i686, thay vì thực sự sử dụng khóa). Hãy thử nó trên liên kết Godbolt Compiler Explorer trong đoạn cuối.

Sử dụng long double/ sqrtl/ explcho độ chính xác cao hơn và độ trễ thêm trong ABI trong đó sizeof ( long double) là 10 hoặc 16 (có phần đệm để căn chỉnh). (IIRC, 64 bit Windows sử dụng 8byte long doubletương đương với double. (Dù sao, tải / lưu trữ các toán hạng FP 10byte (80 bit) là 4/7 uops, so với floathoặc doublechỉ lấy 1 uop mỗi lần cho fld m64/m32/ fst). Buộc x87 với việc long doubletự động hóa vector ngay cả đối với gcc -m64 -march=haswell -O3.

Nếu không sử dụng atomic<uint64_t>bộ đếm vòng lặp, hãy sử dụng long doublecho tất cả mọi thứ, kể cả bộ đếm vòng lặp.

atomic<double>biên dịch, nhưng các thao tác đọc-sửa-ghi như +=không được hỗ trợ cho nó (ngay cả trên 64 bit). atomic<long double>phải gọi một chức năng thư viện chỉ cho tải / cửa hàng nguyên tử. Có lẽ nó thực sự không hiệu quả, vì x86 ISA không hỗ trợ tải / lưu trữ nguyên tử 10byte nguyên tử và cách duy nhất tôi có thể nghĩ đến mà không khóa ( cmpxchg16b) yêu cầu chế độ 64 bit.


Tại -O0, phá vỡ một biểu thức lớn bằng cách gán các bộ phận cho các bình tạm thời sẽ gây ra nhiều lưu trữ / tải lại hơn. Không có volatilehoặc một cái gì đó, điều này sẽ không quan trọng với các cài đặt tối ưu hóa mà một bản dựng thực của mã thực sẽ sử dụng.

Các quy tắc charbí danh cho phép a để bí danh bất cứ điều gì, do đó lưu trữ thông qua một char*trình biên dịch buộc lưu trữ / tải lại mọi thứ trước / sau khi lưu trữ byte, ngay cả tại -O3. (Đây là một vấn đề đối với uint8_t tự động vectơ hoạt động trên một mảng , chẳng hạn.)

Hãy thử uint16_tcác bộ đếm vòng lặp, để buộc cắt ngắn thành 16 bit, có thể bằng cách sử dụng kích thước toán hạng 16 bit (các quầy hàng tiềm năng) và / hoặc các movzxhướng dẫn bổ sung (an toàn). Tràn ký đã ký là hành vi không xác định , vì vậy trừ khi bạn sử dụng -fwrapvhoặc ít nhất -fno-strict-overflow, các bộ đếm vòng lặp đã ký không phải được gia hạn lại mỗi lần lặp , ngay cả khi được sử dụng làm điểm bù cho con trỏ 64 bit.


Buộc chuyển đổi từ số nguyên sang floatvà trở lại một lần nữa. Và / hoặc double<=> floatchuyển đổi. Các hướng dẫn có độ trễ lớn hơn một, và vô hướng int-> float ( cvtsi2ss) được thiết kế xấu để không bằng 0 phần còn lại của thanh ghi xmm. (gcc chèn thêm một phần pxorđể phá vỡ sự phụ thuộc, vì lý do này.)


Thường xuyên đặt mối quan hệ CPU của bạn với một CPU khác (được đề xuất bởi @Egwor). lý luận độc ác: Bạn không muốn một lõi bị quá nóng khi chạy chuỗi của bạn trong một thời gian dài, phải không? Có lẽ việc đổi sang lõi khác sẽ giúp lõi đó tăng tốc lên xung nhịp cao hơn. (Trong thực tế: chúng rất gần nhau đến mức điều này rất khó xảy ra ngoại trừ trong một hệ thống nhiều ổ cắm). Bây giờ chỉ cần điều chỉnh sai và làm điều đó quá thường xuyên. Bên cạnh thời gian dành cho trạng thái lưu / khôi phục hệ điều hành của hệ điều hành, lõi mới có bộ đệm L2 / L1 lạnh, bộ đệm uop và bộ dự báo nhánh.

Giới thiệu các cuộc gọi hệ thống không cần thiết thường xuyên có thể làm bạn chậm lại bất kể chúng là gì. Mặc dù một số quan trọng nhưng đơn giản như gettimeofdaycó thể được triển khai trong không gian người dùng với, không có chuyển đổi sang chế độ kernel. (glibc trên Linux thực hiện điều này với sự trợ giúp của kernel, vì kernel xuất mã trong vdso).

Để biết thêm về chi phí cuộc gọi hệ thống (bao gồm cả bộ nhớ cache / TLB sau khi quay lại không gian người dùng, không chỉ là công tắc ngữ cảnh), bài báo FlexSC có một số phân tích tuyệt vời về tình huống hiện tại, cũng như đề xuất cho hệ thống xử lý theo khối các cuộc gọi từ các quy trình máy chủ đa luồng ồ ạt.


10
@JesperJuhl: yeah, tôi sẽ mua lời biện minh đó. "Không đủ năng lực" là một cụm từ tuyệt vời như vậy :)
Peter Cordes

2
Thay đổi bội số theo hằng số thành chia theo nghịch đảo của hằng số có thể làm giảm hiệu suất một cách khiêm tốn (ít nhất là nếu người ta không cố gắng vượt qua -O3 -fastmath). Tương tự như vậy bằng cách sử dụng tính kết hợp để tăng công việc ( exp(T*(r-0.5*v*v))trở thành exp(T*r - T*v*v/2.0); exp(sqrt(v*v*T)*gauss_bm)trở thành exp(sqrt(v)*sqrt(v)*sqrt(T)*gauss_bm)). Associativity (và khái quát hóa) cũng có thể chuyển đổi exp(T*r - T*v*v/2.0)thành `pow ((pow (e_value, T), r) / pow (pow (pow ((pow (e_value, T), v), v)), - 2.0) [hoặc một cái gì đó như thế]. Những mánh khóe toán học như vậy không thực sự được tính là sự tái cấu trúc vi kiến ​​trúc.
Paul A. Clayton

2
Tôi thực sự đánh giá cao phản hồi này và Fog của Agner đã giúp đỡ rất nhiều. Tôi sẽ để tiêu hóa này và bắt đầu làm việc vào chiều nay. Đây có lẽ là nhiệm vụ hữu ích nhất trong việc thực sự học những gì đang diễn ra.
Cowmoogun

19
Một số trong những lời đề nghị đó không đủ năng lực đến mức tôi phải nói chuyện với giáo sư để xem liệu thời gian chạy 7 phút bây giờ có quá nhiều để anh ta muốn ngồi để xác minh đầu ra không. Vẫn làm việc với điều này, đây có lẽ là điều thú vị nhất tôi từng có với một dự án.
Cowmoogun

4
Gì? Không có đột biến? Có hai triệu luồng chạy đồng thời với một mutex bảo vệ từng tính toán riêng lẻ (chỉ trong trường hợp!) Sẽ đưa siêu máy tính nhanh nhất trên hành tinh đến đầu gối của nó. Điều đó nói rằng, tôi rất thích câu trả lời bất tài này.
David Hammen

35

Một vài điều bạn có thể làm để khiến mọi thứ hoạt động tồi tệ nhất có thể:

  • biên dịch mã cho kiến ​​trúc i386. Điều này sẽ ngăn việc sử dụng SSE và các hướng dẫn mới hơn và buộc sử dụng x87 FPU.

  • sử dụng std::atomicbiến ở khắp mọi nơi. Điều này sẽ làm cho chúng rất tốn kém do trình biên dịch bị buộc phải chèn các rào cản bộ nhớ khắp nơi. Và đây là điều mà một người bất tài có thể làm một cách hợp lý để "đảm bảo an toàn cho luồng".

  • đảm bảo truy cập bộ nhớ theo cách tồi tệ nhất có thể để trình tải trước dự đoán (cột chính so với hàng chính).

  • để làm cho các biến của bạn trở nên đắt hơn, bạn có thể đảm bảo tất cả chúng đều có 'thời lượng lưu trữ động' (phân bổ heap) bằng cách phân bổ chúng newthay vì để chúng có 'thời lượng lưu trữ tự động' (phân bổ ngăn xếp).

  • đảm bảo rằng tất cả bộ nhớ bạn phân bổ được căn chỉnh rất kỳ quặc và bằng mọi cách tránh phân bổ các trang lớn, vì làm như vậy sẽ rất hiệu quả TLB.

  • bất cứ điều gì bạn làm, đừng xây dựng mã của bạn với trình tối ưu hóa trình biên dịch được kích hoạt. Và đảm bảo bật các biểu tượng gỡ lỗi biểu cảm nhất mà bạn có thể (sẽ không làm cho mã chạy chậm hơn, nhưng nó sẽ lãng phí thêm một số dung lượng đĩa).

Lưu ý: Câu trả lời này về cơ bản chỉ tóm tắt ý kiến ​​của tôi rằng @Peter Cordes đã được tích hợp vào câu trả lời rất hay của anh ấy. Đề nghị anh ấy nhận upvote của bạn nếu bạn chỉ có một cái dự phòng :)


9
Sự phản đối chính của tôi đối với một số trong số này là cụm từ của câu hỏi: Để mở rộng chương trình, hãy sử dụng kiến ​​thức của bạn về cách thức hoạt động của đường ống Intel i7 . Tôi không cảm thấy như có bất cứ điều gì cụ thể về x87, hoặc std::atomic, hoặc một mức độ bổ sung thêm từ phân bổ động. Họ cũng sẽ chậm trên một nguyên tử hoặc K8. Vẫn nâng cao, nhưng đó là lý do tại sao tôi chống lại một số đề xuất của bạn.
Peter Cordes

Đó là những điểm công bằng. Bất kể, những điều đó vẫn hoạt động hướng tới mục tiêu của người hỏi phần nào. Đánh giá cao upvote :)
Jesper Juhl

Đơn vị SSE sử dụng các cổng 0, 1 và 5. Đơn vị x87 chỉ sử dụng các cổng 0 và 1.
Michas

@Michas: Bạn đã sai về điều đó. Haswell không chạy bất kỳ hướng dẫn toán học SSE FP nào trên cổng 5. Chủ yếu là các xáo trộn và booleans SSE (xorps / andps / orps). x87 chậm hơn, nhưng lời giải thích của bạn về lý do hơi sai. (Và điểm này là hoàn toàn sai.)
Peter Cordes

1
@Michas: movapd xmm, xmmthường không cần cổng thực thi (được xử lý ở giai đoạn đăng ký đổi tên trên IVB trở lên). Nó hầu như không bao giờ cần thiết trong mã AVX, bởi vì mọi thứ trừ FMA đều không phá hủy. Nhưng đủ công bằng, Haswell chạy nó trên port5 nếu nó không bị loại bỏ. Tôi đã không xem x87 đăng ký sao chép ( fld st(i)), nhưng bạn phù hợp với Haswell / Broadwell: nó chạy trên p01. Skylake chạy nó trên p05, SnB chạy nó trên p0, IvB chạy nó trên p5. Vì vậy IVB / SKL thực hiện một số nội dung x87 (bao gồm so sánh) trên p5, nhưng SNB / HSW / BDW hoàn toàn không sử dụng p5 cho x87.
Peter Cordes

11

Bạn có thể sử dụng long doubleđể tính toán. Trên x86, nó phải là định dạng 80 bit. Chỉ có di sản, x87 FPU có hỗ trợ cho việc này.

Một số thiếu sót của x87 FPU:

  1. Thiếu SIMD, có thể cần thêm hướng dẫn.
  2. Dựa trên ngăn xếp, có vấn đề cho các kiến ​​trúc siêu vô hướng và đường ống.
  3. Bộ thanh ghi riêng biệt và khá nhỏ, có thể cần chuyển đổi nhiều hơn từ các thanh ghi khác và nhiều thao tác bộ nhớ hơn.
  4. Trên Core i7 có 3 cổng cho SSE và chỉ có 2 cổng cho x87, bộ xử lý có thể thực hiện các lệnh ít song song hơn.

3
Đối với toán học vô hướng, bản thân các hướng dẫn toán học x87 chỉ chậm hơn một chút. Tuy nhiên, việc lưu trữ / tải các toán hạng 10byte chậm hơn đáng kể và thiết kế dựa trên ngăn xếp của x87 có xu hướng yêu cầu thêm các hướng dẫn (như fxch). Tuy nhiên -ffast-math, với một trình biên dịch tốt có thể vector hóa các vòng lặp monte-carlo và x87 sẽ ngăn chặn điều đó.
Peter Cordes

Tôi đã mở rộng câu trả lời của tôi một chút.
Michas

1
re: 4: Bạn đang nói về i7 uarch nào, và hướng dẫn nào? Haswell có thể chạy mulsstrên p01, nhưng fmulchỉ trên p0. addsschỉ chạy trên p1, giống nhưfadd . Chỉ có hai cổng thực thi xử lý các op toán học FP. (Ngoại lệ duy nhất là Skylake đã bỏ đơn vị thêm chuyên dụng và chạy addsstrong các đơn vị FMA trên p01, nhưng faddtrên p5. Vì vậy, bằng cách trộn vào một số faddhướng dẫn cùng với fma...ps, về lý thuyết, bạn có thể thực hiện tổng số FLOP / s nhiều hơn một chút.)
Peter Cordes

2
Cũng lưu ý rằng Windows x86-64 ABI có 64 bit long double, tức là nó vẫn chỉ double. SysV ABI không sử dụng 80 bit long double. Ngoài ra, re: 2: đổi tên đăng ký cho thấy sự song song trong các thanh ghi ngăn xếp. Kiến trúc dựa trên ngăn xếp yêu cầu một số hướng dẫn bổ sung, như fxchg, đặc biệt. khi xen kẽ tính toán song song. Vì vậy, thật khó để diễn đạt sự song song mà không có những chuyến đi khứ hồi, thay vì khó để các vị vua khai thác những gì ở đó. Bạn không cần chuyển đổi nhiều hơn từ các reg khác, mặc dù. Không chắc ý của bạn là gì
Peter Cordes

6

Trả lời muộn nhưng tôi không cảm thấy chúng ta đã lạm dụng danh sách liên kết và TLB đủ.

Sử dụng mmap để phân bổ các nút của bạn, sao cho phần lớn bạn sử dụng MSB của địa chỉ. Điều này sẽ dẫn đến chuỗi tra cứu TLB dài, một trang là 12 bit, để lại 52 bit cho bản dịch hoặc khoảng 5 cấp độ mà nó phải đi qua mỗi lần. Với một chút may mắn, họ phải vào bộ nhớ mỗi lần tìm kiếm 5 cấp độ cộng với 1 lần truy cập bộ nhớ để đến nút của bạn, cấp cao nhất rất có thể sẽ nằm trong bộ đệm ở đâu đó, vì vậy chúng tôi có thể hy vọng truy cập bộ nhớ 5 *. Đặt nút sao cho sải bước ở đường viền xấu nhất để đọc con trỏ tiếp theo sẽ gây ra 3-4 lần tra cứu dịch thuật khác. Điều này cũng có thể phá hỏng hoàn toàn bộ đệm do số lượng tra cứu dịch lớn. Ngoài ra, kích thước của các bảng ảo có thể khiến hầu hết dữ liệu người dùng được phân trang vào đĩa để có thêm thời gian.

Khi đọc từ danh sách được liên kết đơn, hãy đảm bảo đọc từ đầu danh sách mỗi lần để gây ra độ trễ tối đa khi đọc một số duy nhất.


Bảng trang x86-64 sâu 4 cấp cho địa chỉ ảo 48 bit. (Một PTE có 52 bit địa chỉ vật lý). Các CPU trong tương lai sẽ hỗ trợ tính năng bảng trang 5 cấp, cho 9 bit không gian địa chỉ ảo khác (57). Tại sao trong 64 bit, địa chỉ ảo ngắn 4 bit (dài 48 bit) so với địa chỉ vật lý (dài 52 bit)? . Các hệ điều hành sẽ không kích hoạt nó theo mặc định vì nó sẽ chậm hơn và không mang lại lợi ích gì trừ khi bạn cần nhiều không gian địa chỉ.
Peter Cordes

Nhưng vâng, ý tưởng vui vẻ. Bạn có thể sử dụng mmaptrên một tệp hoặc vùng bộ nhớ dùng chung để nhận nhiều địa chỉ ảo cho cùng một trang vật lý (có cùng nội dung), cho phép nhiều TLB bỏ lỡ hơn cùng một lượng RAM vật lý. Nếu danh sách liên kết của bạn nextchỉ là phần tương đối , bạn có thể có một loạt ánh xạ của cùng một trang +4096 * 1024cho đến khi cuối cùng bạn đến một trang vật lý khác. Hoặc tất nhiên trải dài trên nhiều trang để tránh các lần truy cập bộ đệm L1d. Có bộ nhớ cache của các PDE cấp cao hơn trong phần cứng đi bộ trang, vì vậy, có thể phát tán nó ra trong không gian bổ sung!
Peter Cordes

Thêm một phần bù vào địa chỉ cũ cũng làm cho độ trễ sử dụng tải trở nên tồi tệ hơn bằng cách đánh bại [trường hợp đặc biệt cho [reg+small_offset]chế độ địa chỉ] ( Có bị phạt khi cơ sở + offset ở một trang khác với cơ sở không? ); bạn sẽ có được nguồn bộ nhớ addcủa phần bù 64 bit hoặc bạn sẽ tải và chế độ địa chỉ được lập chỉ mục như thế nào [reg+reg]. Xem thêm Điều gì xảy ra sau khi bỏ lỡ L2 TLB? - trang đi bộ tìm nạp thông qua bộ đệm L1d trên gia đình SnB.
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.