Мой вопрос является продолжением Как сделать этот код быстрее (изучение лучших практик)?, который был приостановлен (облом). Проблема состоит в том, чтобы оптимизировать цикл над массивом с помощью чисел с плавающей запятой, которые проверяются на предмет того, находятся ли они в заданном интервале. Индексы совпадающих элементов в массиве должны храниться в предоставленном массиве результатов.
Тест включает в себя два условия (меньше верхнего порога и больше нижнего). Очевидный код для теста if( elem <= upper && elem >= lower ) ...
, Я заметил, что ветвление (включая неявную ветвь, участвующую в операторе короткого замыкания&&) намного дороже второго сравнения. То, что я придумал, ниже. Это примерно на 20% -40% быстрее, чем наивная реализация, больше, чем я ожидал. Он использует тот факт, что bool является целочисленным типом. Результат проверки условия используется в качестве индекса для двух массивов результатов. Только один из них будет содержать нужные данные, другой может быть отброшен. Это заменяет структуру программы структурой данных и вычислениями.
Я заинтересован в большем количестве идей для оптимизации. «Технические хаки» (типа, представленного здесь) приветствуются. Меня также интересует, может ли современный C ++ обеспечить средства для ускорения, например, включив компилятор для создания параллельного кода. Подумайте, посетитель шаблон / функтор. Вычисления для отдельных элементов srcArr практически независимы, за исключением того, что порядок индексов в массиве результатов зависит от порядка тестирования элементов исходного массива. Я бы немного ослабил требования, так что порядок сопоставления индексов в массиве результатов не имеет значения. Кто-нибудь может придумать быстрый путь?
Вот исходный код функции. Опорная магистраль находится ниже. gcc нуждается в -std = c ++ 11 из-за хронографа. VS 2013 express также смог скомпилировать это (и создал код на 40% быстрее, чем gcc -O3).
#include <cstdlib>
#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
/// Check all elements in srcArr whether they lie in
/// the interval [lower, upper]. Store the indices of
/// such elements in the array pointed to by destArr[1]
/// and return the number of matching elements found.
/// This has been highly optimized, mainly to avoid branches.
int findElemsInInterval( const float srcArr[], // contains candidates
int **const destArr, // two arrays to be filled with indices
const int arrLen, // length of each array
const float lower, const float upper // interval
)
{
// Instead of branching, use the condition
// as an index into two distinct arrays. We need to keep
// separate indices for both those arrays.
int destIndices[2];
destIndices[0] = destIndices[1] = 0;
for( int srcInd=0; srcInd<arrLen; ++srcInd )
{
// If the element is inside the interval, both conditions
// are true and therefore equal. In all other cases
// exactly one condition is true so that they are not equal.
// Matching elements' indices are therefore stored in destArr[1].
// destArr[0] is a kind of a dummy (it will incidentally contain
// indices of non-matching elements).
// This used to be (with a simple int *destArr)
// if( srcArr[srcInd] <= upper && srcArr[srcInd] >= lower) destArr[destIndex++] = srcInd;
int isInInterval = (srcArr[srcInd] <= upper) == (srcArr[srcInd] >= lower);
destArr[isInInterval][destIndices[isInInterval]++] = srcInd;
}
return destIndices[1]; // the number of elements in the results array
}int main(int argc, char *argv[])
{
int arrLen = 1000*1000*100;
if( argc > 1 ) arrLen = atol(argv[1]);
// destArr[1] will hold the indices of elements which
// are within the interval.
int *destArr[2];
// we don't check destination boundaries, so make them
// the same length as the source.
destArr[0] = new int[arrLen];
destArr[1] = new int[arrLen];
float *srcArr = new float[arrLen];
// Create always the same numbers for comparison (don't srand).
for( int srcInd=0; srcInd<arrLen; ++srcInd ) srcArr[srcInd] = rand();
// Create an interval in the middle of the rand() spectrum
float lowerLimit = RAND_MAX/3;
float upperLimit = lowerLimit*2;
cout << "lower = " << lowerLimit << ", upper = " << upperLimit << endl;
int numInterval;
auto t1 = high_resolution_clock::now(); // measure clock time as an approximation
// Call the function a few times to get a longer run time
for( int srcInd=0; srcInd<10; ++srcInd )
numInterval = findElemsInInterval( srcArr, destArr, arrLen, lowerLimit, upperLimit );
auto t2 = high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( t2 - t1 ).count();
cout << numInterval << " elements found in " << duration << " milliseconds. " << endl;
return 0;
}
Думая о проверке целочисленного диапазона оптимизации поворота <= х && Икс < б в ((без знака) (х-а)) < б-а, на ум приходит вариант с плавающей точкой:
Вы можете попробовать что-то вроде
const float radius = (b-a)/2;
if( fabs( x-(a+radius) ) < radius )
...
сократить чек до одного условного.
Я вижу примерно 10% ускорение от этого:
int destIndex = 0; // replace destIndices
int isInInterval = (srcArr[srcInd] <= upper) == (srcArr[srcInd] >= lower);
destArr[1][destIndex] = srcInd;
destIndex += isInInterval;
Устранить пару выходных массивов. Вместо этого передвигайте «записанное число» на 1, если хотите сохранить результат, в противном случае просто продолжайте перезаписывать индекс «один за концом».
То есть, retval[destIndex]=curIndex; destIndex+= isInArray;
— лучшая согласованность и меньше потерянной памяти.
Напишите две версии: одну, которая поддерживает фиксированную длину массива (скажем, 1024 или любую другую), и другую, которая поддерживает параметр времени выполнения. Использовать template
argumemt для удаления дублирования кода. Предположим, что длина меньше этой константы.
Иметь функцию, возвращающую размер и RVO’d std::array<unsigned, 1024>
,
Напишите функцию-обертку, которая объединяет результаты (создайте все результаты, затем объедините их). Затем добавьте библиотеку параллельных шаблонов в задачу (чтобы результаты вычислялись параллельно).
Если вы разрешите себе векторизацию, используя набор инструкций SSE (или лучше, AVX), вы можете выполнить 4/8 сравнений за раз, сделать это дважды, ‘и’ результаты, и получить 4 результата (-1 или 0). В то же время, это разворачивает цикл.
// Preload the bounds
__m128 lo= _mm_set_ps(lower);
__m128 up= _mm_set_ps(upper);
int srcIndex, dstIndex= 0;
for (srcInd= 0; srcInd + 3 < arrLen; )
{
__m128 src= _mm_load_ps(&srcArr[srcInd]); // Load 4 values
__m128 tst= _mm_and_ps(_mm_cmple_ps(src, lo), _mm_cmpge_ps(src, up)); // Test
// Copy the 4 indexes with conditional incrementation
dstArr[dstIndex]= srcInd++; destIndex-= tst.m128i_i32[0];
dstArr[dstIndex]= srcInd++; destIndex-= tst.m128i_i32[1];
dstArr[dstIndex]= srcInd++; destIndex-= tst.m128i_i32[2];
dstArr[dstIndex]= srcInd++; destIndex-= tst.m128i_i32[3];
}
ВНИМАНИЕ: непроверенный код.