Est-il un moyen fiable pour forcer la GCC (ou n'importe quel compilateur) à facteur d'exécution de la taille des contrôles en memcpy()
à l'extérieur d'une boucle (où que la taille n'est pas constante à la compilation, mais constante à l'intérieur de cette boucle), spécialiste de la boucle pour chaque gamme de taille plutôt qu'à plusieurs reprises la vérification de la taille du sein?
C'est un cas de test réduite à partir d'une régression de la performance signalé ici pour une bibliothèque open source conçue pour l'efficacité en mémoire l'analyse de grands ensembles de données. (La régression se produit à cause d'un de mes commits...)
Le code d'origine est en Cython, mais je l'ai réduite à une pure C proxy comme suit:
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, j, k_local;
k_local = k; /* prevent aliasing */
for(i = 0; i < n; ++i) {
idx = indexer[i];
for(j = 0; j < k_local; ++j)
out[i * stride_out_0 + j * stride_out_1] =
in[idx * stride_in_0 + j * stride_in_1];
}
}
Les progrès sont variables; en général, les tableaux ne sont même pas garanti d'être contiguës (étant donné qu'ils peuvent être non-contigus tranches de grands tableaux). Cependant, pour le cas particulier de c-tableaux contigus, j'ai optimisé le ci-dessus pour les éléments suivants:
void take(double * out, double * in,
int stride_out_0, int stride_out_1,
int stride_in_0, int stride_in_1,
int * indexer, int n, int k)
{
int i, idx, k_local;
assert(stride_out_0 == k);
assert(stride_out_0 == stride_in_0);
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
k_local = k; /* prevent aliasing */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
(Les assertions ne sont pas présents dans le code d'origine; au lieu de cela, il vérifie la continuité et l'appelle la version optimisée si possible, et le unoptimized, si ce n'.)
Cette version optimise très bien dans la plupart des cas, depuis l'utilisation normale si pour petite n
et de grandes k
. Cependant, à l'opposé de cas d'utilisation ne passera (grand n
et petit k
), et il s'avère que pour le cas particulier de l' n == 10000
et k == 4
(qui ne peut pas être exclu en tant que représentant d'une partie importante d'un hypothétique plan de travail), l' memcpy()
version 3.6 x fois plus lent que l'original. C'est, apparemment, principalement en raison du fait qu' k
n'est pas constante à la compilation, comme en témoigne le fait que cette prochaine version effectue (ou presque exactement, en fonction des paramètres d'optimisation) ainsi que l'original (ou mieux, parfois), pour le cas particulier de l' k == 4
:
if (k_local == 4) {
/* this optimizes */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
Bien évidemment, il n'est pas pratique pour coder en dur une boucle pour chaque valeur de k
, j'ai donc tenté le suivant à la place (comme une première tentative qui pourrait par la suite généralisée, si cela a fonctionné):
if (k_local >= 0 && k_local <= 4) {
/* this does not not optimize */
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
memcpy(&out[i * k_local], &in[idx * k_local],
k_local * sizeof(double));
}
}
Malheureusement, cette dernière version n'est plus rapide que l'original memcpy()
version, qui est un peu décourageant pour ma foi en la GCC est l'optimisation des capacités.
Est il possible que je peux donner plus de "trucs" pour GCC (par tous moyens) qui va l'aider à faire la bonne chose ici? (Et même mieux, il y a des "indices" qui pourrait sûrement le travail à travers les différents compilateurs? Cette bibliothèque est compilée pour différentes cibles.)
Les résultats indiqués sont pour GCC 4.6.3 sur 32 bits Ubuntu avec le "-O2" drapeau, mais j'ai aussi testé GCC 4.7.2 et "-O3" versions avec similaire (mais pas identique) des résultats. J'ai posté mon harnais de test pour LiveWorkspace, mais les horaires sont de ma propre machine à l'aide de l' time(1)
commande (je ne sais pas quelle est la fiabilité de LiveWorkspace horaires.)
EDIT: j'ai aussi considéré comme juste, la fixation d'un "nombre magique" pour certains, la taille minimale d'appeler memcpy()
, et que j'ai pu trouver une telle valeur avec des tests répétés, mais je ne suis pas sûr de savoir comment généraliser les résultats de ma recherche serait à travers les différents compilateurs/plates-formes. Est-il une règle de base que je pourrais utiliser ici?
En OUTRE EDIT: Réalisé l' k_local
variables sont plutôt inutile dans ce cas, effectivement, puisque aucun aliasing est possible; il a été réduit à partir de quelques expériences, j'ai couru partout où c'est possible (k
global) et j'ai oublié je l'ai changé. Il suffit de les ignorer cette partie.
MODIFIER la BALISE: Réalisé que je peux aussi utiliser C++ dans des versions plus récentes de Cython, de sorte que le marquage de C++ dans le cas où il ya quelque chose qui peut aider à partir de C++...
Montage FINAL: À la place (pour l'instant) de descendre de l'assemblée spécialisée memcpy()
, la suite semble être la meilleure solution empirique pour ma machine locale:
int i, idx, j;
double * subout, * subin;
assert(stride_out_1 == 1);
assert(stride_out_1 == stride_in_1);
if (k < 32 /* i.e. 256 bytes: magic! */) {
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout = &out[i * stride_out_0];
subin = &in[idx * stride_in_0];
for(j = 0; j < k; ++j)
subout[j] = subin[j];
}
} else {
for(i = 0; i < n; ++i) {
idx = indexer[i];
subout = &out[i * stride_out_0];
subin = &in[idx * stride_in_0];
memcpy(subout, subin, k * sizeof(double));
}
}
Il utilise un "nombre magique" pour décider de l'appel d' memcpy()
ou pas, mais toujours optimise le cas pour les petits tableaux qui sont connus pour être contiguës (donc c'est plus rapide que l'original, dans la plupart des cas, étant donné que l'original ne fait pas une telle hypothèse).