Есть ли какой-либо надежный способ заставить GCC (или любой компилятор) исключить проверки размера времени выполнения в memcpy()
вне цикла (где этот размер не является константой времени компиляции, но константа в этом цикле), специализируясь на цикл для каждого соответствующего диапазона размеров, а не многократно проверять размер внутри него?
Это тестовый пример, уменьшенный по сравнению с регрессией производительности, сообщаемой здесь для библиотеки с открытым исходным кодом, предназначенной для эффективного анализа больших массивов данных в памяти. (Регрессия происходит из-за одного из моих коммитов...)
Исходный код находится в Cython, но я уменьшил его до чистого прокси C следующим образом:
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];
}
}
Шаги являются переменными; в общем случае массивы даже не гарантируются быть смежными (поскольку они могут быть несмежными срезами больших массивов). Однако для частного случая с-смежных массивов я оптимизировал приведенное выше следующее:
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));
}
}
(Утверждений нет в исходном коде, вместо этого он проверяет смежность и вызывает оптимизированную версию, если это возможно, и неоптимизированный, если нет).
Эта версия в большинстве случаев очень хорошо оптимизируется, поскольку обычный вариант использования, если для малых n
и больших k
. Однако имеет место и противоположный случай использования (большой n
и малый k
), и это оказывается для частного случая n == 10000
и k == 4
(который нельзя исключить как представитель важной части гипотетического рабочего процесса), версия memcpy()
в 3,6 раза медленнее оригинала. Это, по-видимому, главным образом из-за того, что k
не является константой времени компиляции, о чем свидетельствует тот факт, что эта следующая версия выполняет (почти или точно, в зависимости от настроек оптимизации), а также оригинальную (или лучше, иногда), для частного случая 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));
}
}
Очевидно, что нецелесообразно жестко кодировать цикл для каждого конкретного значения k
, поэтому я попытался сделать следующее (в качестве первой попытки, которая позже может быть обобщена, если она сработает):
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));
}
}
К сожалению, эта последняя версия не быстрее оригинальной версии memcpy()
, что несколько разочаровывает мою веру в возможности оптимизации GCC.
Есть ли способ дать дополнительные "подсказки" GCC (любыми способами), которые помогут ему сделать правильные вещи здесь? (И еще лучше, есть ли "подсказки", которые могли бы надежно работать с разными компиляторами? Эта библиотека скомпилирована для разных целей.)
Приведенные результаты относятся к GCC 4.6.3 на 32-разрядном Ubuntu с флагом "-O2", но я также тестировал версии GCC 4.7.2 и "-O3" с аналогичными (но не идентичными) результатами. Я отправил свой тестовый жгут в LiveWorkspace, но тайминги взяты из моей машины с помощью команды time(1)
(я не знаю, насколько надежны тайм-ауты LiveWorkspace есть.)
EDIT: Я также рассмотрел возможность установки "магического номера" для некоторого минимального размера для вызова memcpy()
с, и я мог бы найти такое значение при повторном тестировании, но я не знаю, насколько обобщаю мои результаты в разных компиляторах/платформах. Есть ли какое-либо эмпирическое правило, которое я мог бы использовать здесь?
ДАЛЬНЕЙШЕЕ ИЗМЕНИТЬ: Реализованные переменные k_local
в этом случае бесполезны, поскольку наложение не возможно; это было уменьшено из некоторых экспериментов, в которых я работал, где это было возможно (k
был глобальным), и я забыл, что я его изменил. Просто проигнорируйте эту часть.
EDIT TAG: Реализовано Я также могу использовать С++ в более новых версиях Cython, поэтому пометка как С++ в случае, если что-нибудь может помочь с С++...
ЗАКЛЮЧИТЕЛЬНЫЙ РЕДАКТ: Вместо того, чтобы сбрасываться на сборку для специализированного memcpy()
, кажется лучшим эмпирическим решением для моей локальной машины:
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));
}
}
Это использует "магическое число", чтобы решить, следует ли вызывать memcpy()
или нет, но все же оптимизирует случай для небольших массивов, которые, как известно, являются смежными (поэтому он быстрее, чем оригинал, в большинстве случаев, поскольку оригинал не делает такого предположения).