Cela fait une semaine que je me suis creusé la tête en essayant de mener à bien cette mission et j'espère que quelqu'un ici pourra me guider vers le bon chemin. Permettez-moi de commencer par les instructions de l'instructeur:
Votre tâche est à l’opposé de notre première tâche en laboratoire, qui visait à optimiser un programme de nombres premiers. Votre but dans cette mission est de pessimiser le programme, c’est-à-dire de le ralentir. Ces deux sont des programmes gourmands en ressources processeur. Ils prennent quelques secondes pour fonctionner sur nos ordinateurs de laboratoire. Vous ne pouvez pas changer l'algorithme.
Pour désoptimiser le programme, utilisez vos connaissances du fonctionnement du pipeline Intel i7. Imaginez des moyens de réordonner les chemins d'instructions pour introduire WAR, RAW et d'autres dangers. Pensez à des moyens de minimiser l'efficacité du cache. Être diaboliquement incompétent.
La mission donnait le choix entre des programmes Whetstone et Monte-Carlo. Les commentaires sur l'efficacité du cache ne s'appliquent généralement qu'à Whetstone, mais j'ai choisi le programme de simulation de 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;
}
Les modifications que j'ai apportées semblent augmenter la durée d'exécution du code d'une seconde, mais je ne suis pas tout à fait sûr de ce que je peux changer pour bloquer le pipeline sans ajouter de code. Un point dans la bonne direction serait génial, j'apprécie toutes les réponses.
Les points forts sont:
CPUID
et à la détermination de la taille du cache, ainsi qu'aux éléments intrinsèques et à l'instruction CLFLUSH
.Les commentaires de Cowmoogun sur le méta-thread indiquent que il n'était pas clair que les optimisations du compilateur pourraient en faire partie, et supposaient que -O0
, et qu'une augmentation de 17% du temps d'exécution était raisonnable.
Il semble donc que l'objectif de la tâche était d'amener les étudiants à réorganiser le travail existant pour réduire le parallélisme au niveau de l'instruction ou des choses du même genre, mais ce n'est pas une mauvaise chose que les gens aient approfondi et appris davantage.
N'oubliez pas qu'il s'agit d'une question d'architecture informatique, et non de la façon de ralentir le C++ en général.
Quelques choses que vous pouvez faire pour que les choses fonctionnent aussi mal que possible:
compilez le code pour l'architecture i386. Cela empêchera l'utilisation de SSE et des instructions plus récentes et forcera l'utilisation de la FPU x87.
utilisez std::atomic
variables partout. Cela les rendra très coûteux car le compilateur est obligé d'insérer des barrières de mémoire partout. Et c’est quelque chose qu’une personne incompétente pourrait faire de façon plausible pour "assurer la sécurité du fil".
assurez-vous que le préfetcher accède à la mémoire de la pire façon possible (colonne majeure vs ligne principale).
pour rendre vos variables plus coûteuses, vous pouvez vous assurer qu'elles ont toutes une "durée de stockage dynamique" (allocation de segment de mémoire) en les affectant avec new
plutôt que de leur laisser une "durée de stockage automatique" (pile allouée).
assurez-vous que toute la mémoire que vous allouez est très bizarrement alignée et évitez, par tous les moyens, d'allouer des pages volumineuses, car cela serait beaucoup trop efficace.
quoi que vous fassiez, ne construisez pas votre code avec l'optimiseur de compilateurs activé. Et assurez-vous d'activer les symboles de débogage les plus expressifs que vous pouvez (cela ne rendra pas le code exécuté plus lent, mais vous perdrez de l'espace disque supplémentaire).
Remarque: cette réponse ne fait que résumer mes commentaires que Peter Cordes a déjà intégrés à sa très bonne réponse. Suggérez-lui d'avoir votre vote positif si vous n'en avez qu'un seul :)
Vous pouvez utiliser long double
pour le calcul. Sur x86, ce devrait être le format 80 bits. Seul l'héritage, x87 FPU prend en charge cela.
Quelques défauts de x87 FPU:
Réponse tardive, mais je ne pense pas que nous ayons assez abusé des listes chaînées et du TLB.
Utilisez mmap pour allouer vos nœuds, de manière à ce que vous utilisiez principalement le MSB de l'adresse. Cela devrait entraîner de longues chaînes de recherche TLB, une page de 12 bits, ce qui laisse 52 bits pour la traduction, ou environ 5 niveaux qu'elle doit parcourir à chaque fois. Avec un peu de chance, ils doivent aller en mémoire à chaque fois pour une recherche sur 5 niveaux plus un accès en mémoire pour accéder à votre nœud. Placez le nœud de manière à ce que la bordure la plus mauvaise franchisse la frontière, de sorte que la lecture du pointeur suivant provoquerait 3 ou 4 recherches de traduction supplémentaires. Cela pourrait également totalement détruire le cache en raison de la quantité massive de recherches de traduction. De plus, la taille des tables virtuelles peut entraîner une pagination de la plupart des données utilisateur sur le disque.
Lors de la lecture à partir de la liste chaînée unique, veillez à lire chaque fois au début de la liste pour que le délai de lecture d’un nombre unique soit maximal.