Самый быстрый способ определить, находится ли целое число между двумя целыми числами (включительно) с известными наборами значений

350

Есть ли более быстрый способ, чем x >= start && x <= end в C или С++, проверить, существует ли целое число из двух целых чисел?

UPDATE: Моя конкретная платформа - iOS. Это часть функции размытия ящика, которая ограничивает пиксели по кругу в заданном квадрате.

UPDATE: после ответа принятого ответа я получил ускорение порядка одной строки кода, выполнив обычный способ x >= start && x <= end.

UPDATE: вот код после и после с ассемблером из XCode:

NEW WAY

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

OLD WAY

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

Довольно удивительно, как уменьшение или устранение ветвления может обеспечить такую ​​резкую скорость.

  • 26
    Почему вы обеспокоены тем, что это не достаточно быстро для вас?
  • 10
    Является ли этот конкретный тест узким местом в вашем приложении?
Показать ещё 28 комментариев
Теги:
performance
math

5 ответов

502
Лучший ответ

Есть старый трюк, чтобы сделать это только с одним сравнением/ветвью. Будет ли это действительно улучшать скорость, может быть открытым вопрос, и даже если это произойдет, это, вероятно, слишком мало, чтобы заметить или позаботиться, но когда вы только начинаете с двух сравнений, шансы на значительное улучшение довольно удалены. Код выглядит так:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

С типичным современным компьютером (т.е. чем-либо, использующим два дополнения) преобразование в unsigned действительно является nop - просто изменение в том, как просматриваются одни и те же биты.

Обратите внимание, что в типичном случае вы можете предварительно вычислить upper-lower вне цикла (предполагаемого), так что обычно не вносят сколько-нибудь значительного времени. Наряду с уменьшением количества команд ветвления, это также (как правило) улучшает предсказание ветвей. В этом случае выполняется одна и та же ветвь, находится ли это число ниже нижнего конца или над верхним концом диапазона.

Что касается этого, основная идея довольно проста: отрицательное число, если смотреть как беззнаковое число, будет больше, чем все, что начиналось как положительное число.

На практике этот метод переводит number и интервал в точку начала и проверяет, находится ли number в интервале [0, D], где D = upper - lower. Если number ниже нижней границы: отрицательный, а если выше верхней границы: больше, чем D.

  • 0
    Довольно аккуратно. Но это зависит от того, переполнен ли upper-lower ...
  • 2
    @OliCharlesworth: Да, но он сказал, что оба больше 0, поэтому он не может переполниться (то есть, верхний нижний <верхний).
Показать ещё 30 комментариев
17

Это зависит от того, сколько раз вы хотите выполнить тест по тем же данным.

Если вы выполняете тест за один раз, вероятно, нет значимого способа ускорить алгоритм.

Если вы делаете это для очень конечного набора значений, вы можете создать таблицу поиска. Выполнение индексации может быть более дорогостоящим, но если вы можете поместить всю таблицу в кеш, вы можете удалить все ветвления из кода, что должно ускорить процесс.

Для ваших данных таблица поиска будет 128 ^ 3 = 2,097,152. Если вы можете управлять одной из трех переменных, поэтому учитывайте все экземпляры, где start = N за один раз, размер рабочего набора падает до 128^2 = 16432 байтов, что должно хорошо вписываться в большинство современных кешей.

Вам все равно нужно будет сравнить фактический код, чтобы увидеть, является ли таблица нерассмотренного поиска достаточно быстрой, чем очевидные сравнения.

  • 0
    Таким образом, вы бы сохранили какой-то поиск по заданному значению, start и end, и он содержал бы BOOL, сообщающий вам, находится ли он между ними?
  • 0
    Правильный. Это будет таблица поиска в 3D: bool between[start][end][x] . Если вы знаете, как будет выглядеть ваш шаблон доступа (например, x монотонно увеличивается), вы можете спроектировать таблицу, чтобы сохранить локальность, даже если вся таблица не помещается в памяти.
Показать ещё 1 комментарий
17

Редко можно сделать значительную оптимизацию для кода в таком маленьком масштабе. Большой прирост производительности достигается благодаря наблюдению и изменению кода с более высокого уровня. Возможно, вам удастся полностью исключить необходимость теста диапазона или использовать только O (n) вместо O (n ^ 2). Вы можете повторно заказать тесты, чтобы одна сторона неравенства всегда подразумевалась. Даже если алгоритм идеален, выигрыши с большей вероятностью появятся, когда вы увидите, как этот код проверяет диапазон 10 миллионов раз, и вы найдете способ их пакетного запуска и использовать SSE для параллельного выполнения многочисленных тестов.

  • 15
    Несмотря на отрицательные отзывы, я поддерживаю свой ответ: сгенерированная сборка (см. Ссылку на вставку в комментарии к принятому ответу) довольно ужасна для чего-то во внутреннем цикле функции обработки пикселей. Принятый ответ - изящный трюк, но его драматический эффект намного превосходит то, что разумно ожидать для устранения доли ветвления за итерацию. Некоторый вторичный эффект является доминирующим, и я все еще ожидаю, что попытка оптимизировать весь процесс по сравнению с этим одним тестом оставит преимущества умного сравнения диапазонов в пыли.
1

Этот ответ должен сообщить о тестировании, выполненном с принятым ответом. Я выполнил закрытый тест диапазона на большом векторе отсортированного случайного целого числа, и, к моему удивлению, основной метод (low <= num & num <= high) на самом деле быстрее, чем принятый ответ выше! Тест проводился на HP Pavilion g6 (AMD A6-3400APU с емкостью 6 ГБ. Здесь основной код, используемый для тестирования:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

по сравнению со следующим, который является принятым ответом выше:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

Обратите внимание, что randVec - отсортированный вектор. Для любого размера MaxNum первый метод превосходит второй на моей машине!

  • 0
    Мои данные не отсортированы, и мои тесты на iPhone руку процессора. Ваши результаты с разными данными и процессором могут отличаться.
  • 0
    отсортировано в моем тесте только для того, чтобы убедиться, что верхний предел не меньше нижнего.
Показать ещё 1 комментарий
-1

Невозможно просто выполнить побитовое действие над целым?

Так как он должен быть между 0 и 128, если установлен 8-й бит (2 ^ 7), то он равен 128 или больше. Случай с краем будет больно, однако, поскольку вы хотите инклюзивное сравнение.

  • 2
    Он хочет знать, если x <= end , где end <= 128 . Не x <= 128 .
  • 1
    Это утверждение « Поскольку оно должно быть между 0 и 128, если 8-й бит установлен (2 ^ 7), то это 128 или более », неверно. Рассмотрим 256.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню