Как быстро вы можете сделать линейный поиск?

Я хочу оптимизировать этот линейный поиск:

static int
linear (const int *arr, int n, int key)
{
        int i = 0;
        while (i < n) {
                if (arr [i] >= key)
                        break;
                ++i;
        }
        return i;
}

Массив отсортирован, и функция должна вернуть индекс первого элемента, который больше или равен ключу. Их массив невелик (менее 200 элементов) и будет подготовлен один раз на большое количество поисков. Элементы массива после n-го можно при необходимости инициализировать чем-то подходящим, если это ускорит поиск.

Нет, бинарный поиск не разрешен, только линейный поиск.

Правка: теперь собраны все мои знания по этой теме в этом сообщении блога.


person Mark Probst    schedule 30.04.2010    source источник
comment
Единственное, что вы можете сделать, это воспользоваться любыми инструкциями SIMD, доступными на вашей платформе. (Например, проверьте четыре за раз.) Хотя почему бы вам не использовать бинарный поиск, я не знаю.   -  person GManNickG    schedule 30.04.2010
comment
Вам не нужно тестировать каждый элемент; вы можете проверить каждый k-й элемент, если вам будет позволено вернуться назад. Кроме того, если вы знаете диапазон элементов, вы можете настроить массив/хэш-таблицу, которая просто даст вам ответ. Но вы можете не рассматривать этот линейный поиск.   -  person Nathan S.    schedule 30.04.2010
comment
Почему бинарный поиск (произвольно?) не разрешен? Это настоящая проблема или какое-то домашнее задание? Потому что, если вы собираетесь столкнуться с проблемой сортировки данных, бинарный поиск будет вашим лучшим решением.   -  person Joe    schedule 30.04.2010
comment
(Случайная мысль: один два, пропустить несколько - но это может попасть в категорию «нелинейный», и если оно уже отсортировано, учитывая любое нетривиальное n без совпадения, ожидаемого рядом с фронтом, и связанных с ним проблем с локацией...)   -  person    schedule 30.04.2010
comment
@pst, это в основном бинарный поиск, где несколько - это оставшийся размер массива / 2.   -  person strager    schedule 30.04.2010
comment
@Joe: Двоичный поиск будет работать лучше всего, только если вы знаете, что искомый элемент находится в совершенно непредсказуемом месте. Если вы знаете, что целевой элемент, вероятно, будет близок к началу массива, линейный поиск превзойдет бинарный поиск. Классический пример — всем известный алгоритм слияния двух отсортированных массивов. Если массивы имеют примерно одинаковую длину, объединение выполняется с линейным поиском, так как двоичный поиск будет намного медленнее.   -  person AnT    schedule 30.04.2010
comment
Помните, что бинарный поиск требует, чтобы ваш набор данных был отсортирован.   -  person BobbyShaftoe    schedule 30.04.2010
comment
на самом деле, я где-то читал, что для небольшого массива линейный поиск может быть быстрее: lwn.net/Articles/255364 (обсуждение в комментариях)   -  person Anycorn    schedule 30.04.2010
comment
Будет ли считаться читерством, если вы просканируете сначала каждый десятый элемент и, найдя первый не меньше ключа, вернетесь назад и просканируете последние десять элементов один за другим? Как насчет квадратного корня из n вместо 10?   -  person Maciej Hehl    schedule 30.04.2010
comment
@AndreyT - это отлично подходит для конкретных случаев, но ничто не указывает на то, что в этом наборе данных есть что-то особенное. В общем случае отсортированный, но в остальном скучный набор данных будет работать лучше всего в большинстве случаев использования с бинарным поиском.   -  person Joe    schedule 30.04.2010
comment
Да, не сканировать каждый элемент было бы обманом. @GMan: Вы можете многое сделать, прежде чем прибегать к SIMD. @Joe: Это домашнее задание, которое я задал себе и уже сделал. Мне просто любопытно, что люди придумали, о чем я не подумал.   -  person Mark Probst    schedule 30.04.2010
comment
@Mark: Все, что вы делаете, по сути, будет бинарным поиском.   -  person GManNickG    schedule 30.04.2010
comment
@GMan: см., например, ответ с самым высоким рейтингом. Гораздо быстрее, чем простой линейный поиск (я знаю, я сравнивал).   -  person Mark Probst    schedule 30.04.2010
comment
Я удивлен этим. Какая разница, если развернуть четыре?   -  person GManNickG    schedule 30.04.2010
comment
Развертка в четыре раза ускоряет почти на 50% при N=100 на Core i7. Развертывание вчетвером с часовым ускоряется более чем на 50%.   -  person Mark Probst    schedule 30.04.2010
comment
Все еще нет решений с использованием нескольких потоков?   -  person too much php    schedule 30.04.2010
comment
@Mark Probst: Да, развертывание может ускорить процесс, думаю, мне нужно перечитать мою книгу Code Complete :-) Вот тема развертывания из этой книги stevemcconnell.com/cctune.htm   -  person Michael Buen    schedule 30.04.2010
comment
@Joe: Да, но посмотрите на проблему: они должны реализовать линейный поиск в отсортированном массиве. Это уже достаточно неуниверсально. Это уже подразумевает что-то конкретное. Зачем кому-то настаивать на линейном поиске в отсортированном массиве? Может быть, они делают это потому, что структура запросов благоприятствует именно линейному поиску? Например, если у вас есть упорядоченный массив из N элементов и вам нужно выполнить почти N упорядоченных поисковых запросов, пошаговый линейный поиск превзойдет бинарный поиск как минимум на порядок.   -  person AnT    schedule 30.04.2010
comment
@Mark Probst: вы компилируете с включенной оптимизацией, верно?   -  person Bastien Léonard    schedule 30.04.2010
comment
бинарный поиск с некоторым априорным знанием распределения данных в большинстве случаев будет все же быстрее, чем линейный поиск; проще всего использовать расстояния для линейной интерполяции точки разделения для следующей итерации вместо перехода к середине диапазона (это лучше всего работает, если данные распределены равномерно, в других случаях формула интерполяции должна быть близка к этому распределению)   -  person fortran    schedule 30.04.2010
comment
Если бы кто-то настаивал на использовании линейного поиска по отсортированным данным, я бы просто вернул случайный результат, потому что такой глупец не заметил бы разницы.   -  person mikerobi    schedule 01.05.2010
comment
@fortran: Что ж, если вам нужно сделать M отсортированных запросов к массиву из N отсортированных элементов, асимптотически оптимальный алгоритм работает следующим образом: сначала мы выполняем распределенные линейный поиск с шагом [N/M], т.е. выполнить линейный поиск с пропуском каждого [N/M]-го элемента, а затем выполнить бинарный поиск в найденном отрезке длины [N /М]. Когда M близко к N, [N/M] становится маленьким, и бинарный поиск отключается. Так вот, никакой бинарный поиск, при вышеперечисленных условиях (т.е. достаточно плотно отсортированные запросы к одним и тем же данным) не будет быстрее линейного поиска. Линейный поиск будет намного быстрее.   -  person AnT    schedule 01.05.2010
comment
@fortran: доказано, что приведенная выше комбинация линейного поиска с разбросом и бинарного поиска является асимптотически оптимальным алгоритмом, который достигает теоретического предела эффективности поиска. Таким образом, никакой бинарный поиск не работает быстрее, только когда запросы разрежены. С плотными запросами линейный поиск выигрывает с огромным отрывом. И, опять же, для промежуточных случаев оптимальный алгоритм использует их сочетание. Теоретический результат доступен из этой статьи: citeseerx.ist.psu .edu/viewdoc/summary?doi=10.1.1.22.5750   -  person AnT    schedule 01.05.2010
comment
@mikerobi: Что, вероятно, сделало бы вас гораздо более удивительным, если бы вы узнали, что пошаговый линейный поиск по отсортированным данным имеет смысл, когда нам нужно сделать несколько отсортированных запросов. Когда количество запросов приближается к размеру данных, линейный поиск на порядок превосходит бинарный поиск. Более того, по этой причине практически все используют его (то есть линейный поиск) при слиянии отсортированных данных. Они просто этого не осознают.   -  person AnT    schedule 01.05.2010
comment
@Suma, это не проблема [code-golf]. Если бы он был оставлен с таким тегом, он был бы закрыт, потому что проблемы с кодовым гольфом, которые не являются CW, постоянно закрываются.   -  person Earlz    schedule 01.05.2010
comment
@AndreyT, объединение отсортированных данных занимает линейное время, но это не линейный поиск. Вы правы в отношении поиска нескольких значений одновременно, но это не было частью постановки задачи OP.   -  person mikerobi    schedule 01.05.2010
comment
@mikerobi: Да, это линейный поиск :) Классический алгоритм слияния отсортированных массивов основан на взятии текущего минимального элемента из двух массивов и отправке его на выход. Это не очевидно, но на самом деле это не что иное, как обычный линейный поиск элементов из одного массива в другом массиве. Он просто немного запутан, так что сразу его не увидишь, но на самом деле это простой и понятный линейный поиск.   -  person AnT    schedule 01.05.2010
comment
Более того, та же логика применима и к слиянию: если вы объединяете два массива существенно разной длины, лучше переключиться на двоичный поиск для слияния. Но если длины примерно одинаковые, используем классический алгоритм с линейным поиском.   -  person AnT    schedule 01.05.2010
comment
@AndreyT, я не согласен, поскольку списки отсортированы, у вас есть указатели на самый маленький (или самый большой) элемент в каждом, поэтому вы никогда не ищете следующий элемент одного из подсписков, вы только решаете, какой подсписок взять элемент из.   -  person mikerobi    schedule 02.05.2010
comment
@mikerobi: Нет, ты просто настаиваешь на одном конкретном видении того, что происходит. Вот альтернативное видение: для выполнения слияния мы берем первый элемент a из отсортированного массива A и выполняем линейный поиск этого элемента в отсортированном массиве B. Это дает нам [возможно, пустую] последовательность ведущих элементов в B, которые меньше, чем a. Мы перемещаем всю эту последовательность на выход, за которой следует a. Затем повторяем: берем следующий элемент a из A... и так далее. Вот и все.   -  person AnT    schedule 02.05.2010
comment
На первый взгляд это может показаться другим алгоритмом, но если вы немного подумаете, то увидите, что это точно тот же классический алгоритм слияния, только описанный в других терминах :) Опять же , классический алгоритм слияния представляет собой не более чем слегка запутанный линейный поиск. И снова, если массивы имеют разную длину, правильный способ выполнить слияние — использовать бинарный поиск: взять a из A, выполнить бинарный поиск в B, переместить начиная с B до вывода, перемещайте a до вывода, повторяйте.   -  person AnT    schedule 02.05.2010
comment
И снова универсальная асимптотически оптимальная стратегия представляет собой смесь линейного и бинарного поиска, как описано в моем ответе ниже.   -  person AnT    schedule 02.05.2010
comment
Я голосую за то, чтобы закрыть этот вопрос как не относящийся к теме, потому что он лучше подходит для Code Review.   -  person double-beep    schedule 21.07.2019
comment
@double-beep: это не запрос на проверку кода простого скалярного линейного поиска, он использует его для описания / демонстрации алгоритма, который нужно векторизовать. Кроме того, он слишком стар для миграции и имеет существующие ответы, поэтому, если этот первый аргумент вас не убедил, я бы все же предложил сделать исключение из правила для этого исторического вопроса.   -  person Peter Cordes    schedule 08.08.2019


Ответы (19)


  1. Скажите своему боссу, что вы можете сделать это на 50% быстрее, но это займет 6 месяцев и немного денег.
  2. Подожди полгода.
  3. Купить новое оборудование.

Что ж, это имеет примерно такой же смысл, как и линейный поиск в отсортированном массиве!

(А если серьезно, можете ли вы дать нам некоторые подсказки о том, почему нет бинарного поиска?)

person Oddthinking    schedule 30.04.2010
comment
Если вы прочитаете все комментарии, то увидите, что он спросил это в качестве умственного упражнения. Мне нравится твой ответ, это классика! Определенно мыслить нестандартно. К сожалению, на самом деле это не затрагивает суть вопроса, а именно то, как вы могли бы написать код по-другому. - person Mark Ransom; 30.04.2010
comment
Я могу превзойти @Mark Ransom... просто разгони свой процессор. - person rlbond; 30.04.2010
comment
Андрей, я проголосовал за ваш комментарий. Конечно, вы правы, это не полезно. Где-то здесь есть мета-дискуссия о том, сколько усилий сообщество должно приложить к проблемам, которые на самом деле являются самостоятельными мыслительными упражнениями и не помечены как головоломка/гольф или что-то в этом роде. - person Oddthinking; 30.04.2010
comment
Я просто пошел и посмотрел на его сообщение в блоге. Там приводится его оправдание. А его блог назван в честь четырех моих любимых занятий. Теперь мне плохо :-( - person Oddthinking; 01.05.2010

До сих пор вы получали несколько советов, большинство из которых утверждают, что линейный поиск не имеет смысла для отсортированных данных, когда вместо этого бинарный поиск будет работать гораздо эффективнее. Это часто оказывается одним из тех популярных утверждений о том, что «звучит правильно», сделанных людьми, которые не слишком задумываются над проблемой. На самом деле, если рассматривать более широкую картину, при правильных обстоятельствах линейный поиск может быть намного эффективнее бинарного поиска.

Обратите внимание, что если мы рассматриваем одиночный поисковый запрос для отсортированного массива, бинарный поиск является значительно более эффективным методом, чем линейный поиск. Об этом не спорят. Кроме того, когда вы выполняете несколько полностью случайных запросов к одним и тем же данным, бинарный поиск по-прежнему имеет преимущество перед линейным поиском.

Однако картина начинает меняться, если рассматривать последовательные поисковые запросы, а эти запросы не совсем случайны. Представьте, что запросы поступают в отсортированном порядке, т. е. каждый следующий запрос имеет более высокое значение, чем предыдущий запрос. т.е. запросы также сортируются. Кстати, они не должны быть глобально и строго отсортированы, время от времени последовательность запросов может «сбрасываться», то есть запрашивается низкое значение, но в среднем последующие запросы должны приходить в возрастающем порядке. Другими словами, запросы поступают сериями, каждая серия отсортирована в возрастающем порядке. В этом случае, если средняя длина ряда сравнима с длиной вашего массива, линейный поиск превзойдет бинарный поиск с огромным отрывом. Однако, чтобы воспользоваться этой ситуацией, вы должны реализовать поиск поэтапно. Все просто: если следующий запрос больше предыдущего, не нужно начинать поиск с начала массива. Вместо этого вы можете искать с того места, где остановился предыдущий поиск. Самая упрощенная реализация (просто для иллюстрации идеи) может выглядеть следующим образом.

static int linear(const int *arr, int n, int key)
{
  static int previous_key = INT_MIN;
  static int previous_i = 0;

  i = key >= previous_key ? previous_i : 0;

  while (i < n) {
    if (arr[i] >= key)
      break;
    ++i;
  }

  previous_key = key;
  previous_i = i;

  return i;
}

(Отказ от ответственности: приведенная выше реализация ужасно уродлива по той очевидной причине, что массив поступает извне в качестве параметра, а предыдущее состояние поиска сохраняется внутри. Конечно, это неправильный способ сделать это на практике. Но опять же, выше предназначен для иллюстрации идеи и не более).

Обратите внимание, что сложность обработки каждой серии упорядоченных запросов с использованием описанного выше подхода всегда равна O(N), независимо от длины серии. При использовании бинарного поиска сложность будет O(M * log N). Итак, по понятным причинам, когда M близко к N, т.е. запросы поступают достаточно длинными упорядоченными сериями, вышеописанный линейный поиск будет значительно превосходить бинарный поиск, а при малых M бинарный поиск будет выигрывать.

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

P.S. В качестве дополнительной информации о структуре проблемы:

Когда вам нужно выполнить поиск в упорядоченном массиве длины N и вы заранее знаете, что запросы будут приходить упорядоченными сериями [приблизительной, средней] длины M, оптимальный алгоритм будет выглядеть следующим образом

  1. Рассчитайте значение шага S = [N/M]. Также может иметь смысл «привязать» значение S к [ближайшей] степени числа 2. Думайте о своем отсортированном массиве как о последовательности блоков длиной S — так называемых S-блоках.
  2. Получив запрос, выполнить пошаговый линейный поиск S-блока, потенциально содержащего запрашиваемое значение, т.е. это обычный линейный поиск с шагом S (разумеется, не забудьте начать с блока, в котором предыдущий поиск остановлен).
  3. Найдя S-блок, выполните двоичный поиск в S-блоке запрошенного значения.

Вышеприведенный алгоритм является наиболее оптимальным из возможных алгоритмов пошагового поиска в том смысле, что он достигает теоретического предела асимптотической эффективности повторяющегося поиска. Обратите внимание: если значение M намного меньше, чем N, алгоритм "автоматически" переключается в сторону бинарного поиска, а когда M приближается к N, алгоритм "автоматически" отдает предпочтение линейному< /em> поиск. Последнее имеет смысл, поскольку в такой среде линейный поиск значительно эффективнее бинарного поиска.

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

person AnT    schedule 30.04.2010
comment
Я думаю, что это лучший ответ, поскольку ОП сказал о большом количестве поисков. - person temp2290; 03.05.2010
comment
Связано: Каков наиболее эффективный способ реализации BST таким образом, чтобы функция поиска (значения) оптимизировалась для случайных значений в дереве на x86? бинарное дерево поиска не всегда является лучшей структурой данных для случаев, когда линейность не годится. N-арное дерево, где N-1 кратно ширине вектора SIMD, обеспечивает эффективный поиск на современном x86. например 17-ричный для 4x 4-элементных векторов SIMD с гораздо лучшей пространственной локализацией, чем бинарный поиск по отсортированному массиву, и меньшим количеством шагов. SIMD также может быть очень полезен для линейного поиска. - person Peter Cordes; 08.08.2019

Поскольку вы можете поместить известные значения после последней допустимой записи, добавьте дополнительный элемент n+1 = max, чтобы убедиться, что цикл не выходит за конец массива без проверки i ‹ n.

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (arr[i] < key) {
                ++i;
        }
        return i;
}

Вы также можете попробовать развернуть цикл с тем же сигнальным значением:

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (true) {
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
                if (arr [i++] >= key)
                        break;
        }
        return --i;
}
person Mark Ransom    schedule 30.04.2010
comment
Верно в принципе, но неверно в деталях. Дозор должен быть больше или равен ключу, а не меньше. - person Mark Probst; 30.04.2010
comment
Потребовалось несколько правок, чтобы сделать это правильно, извините, если кто-то запутался. - person Mark Ransom; 30.04.2010
comment
Кроме того, утверждение неверно, кроме знака. Элемент после последнего имеет индекс n, а не n+1. - person Mark Probst; 30.04.2010
comment
@Марк, спасибо, что заметил n+1, наверное, я еще не закончил редактирование. И я думаю, что вы правы и насчет часового, так оно и было у меня сначала — я пытаюсь сделать это слишком быстро. - person Mark Ransom; 30.04.2010
comment
Пока мы занимаемся микрооптимизацией, вы также можете начать i с -1, а затем предварительно увеличить его в индексации массива в вашем примере с развернутым циклом. Это сэкономит вам дополнительные --i в конце. - person mbauman; 30.04.2010
comment
@Matt, поскольку декремент находится вне цикла, он не окажет заметного влияния на результаты. Мне также нужно было бы изменить все постинкременты на преинкременты. Спасибо за предложение. - person Mark Ransom; 01.05.2010
comment
@Mark Ransom: Зачем разворачивать 4 раза? Почему не 2 или 8 или 16? - person Lazer; 13.06.2010
comment
@Lazer, вы, конечно, могли бы это сделать, но есть точка убывающей отдачи. Кроме того, чем больше код, тем больше вероятность проблем с кешем. - person Mark Ransom; 13.06.2010
comment
@Mark Ransom: да, я это понимаю, но как ты все-таки пришел к 4? Кроме того, я не уверен в конечной части For n ‹ 4 такое развертывание, конечно, совсем не ускорит поиск. - person Lazer; 13.06.2010

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

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

Однако линейный поиск можно реализовать вообще без ветвлений. Идея очень проста: индекс, который вам нужен, — это именно то количество элементов в массиве, которое меньше, чем ключ, который вы ищете. Таким образом, вы можете сравнить каждый элемент массива со значением ключа и суммировать все флаги:

static int linear_stgatilov_scalar (const int *arr, int n, int key) {
    int cnt = 0;
    for (int i = 0; i < n; i++)
        cnt += (arr[i] < key);
    return cnt;
}

Самое интересное в этом решении то, что оно вернет один и тот же ответ, даже если вы перемешаете массив =) Хотя это решение кажется довольно медленным, его можно элегантно векторизовать. Реализация, представленная ниже, требует, чтобы массив был выровнен по 16 байтам. Кроме того, массив должен быть дополнен элементами INT_MAX, поскольку он использует 16 элементов одновременно.

static int linear_stgatilov_vec (const int *arr, int n, int key) {
    assert(size_t(arr) % 16 == 0);
    __m128i vkey = _mm_set1_epi32(key);
    __m128i cnt = _mm_setzero_si128();
    for (int i = 0; i < n; i += 16) {
        __m128i mask0 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+0]), vkey);
        __m128i mask1 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+4]), vkey);
        __m128i mask2 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+8]), vkey);
        __m128i mask3 = _mm_cmplt_epi32(_mm_load_si128((__m128i *)&arr[i+12]), vkey);
        __m128i sum = _mm_add_epi32(_mm_add_epi32(mask0, mask1), _mm_add_epi32(mask2, mask3));
        cnt = _mm_sub_epi32(cnt, sum);
    }
    cnt = _mm_hadd_epi32(cnt, cnt);
    cnt = _mm_hadd_epi32(cnt, cnt);
//  int ans = _mm_extract_epi32(cnt, 0);    //SSE4.1
    int ans = _mm_extract_epi16(cnt, 0);    //correct only for n < 32K
    return ans;
}

Окончательное сокращение одного регистра SSE2 можно реализовать с помощью SSE2 только в случае необходимости, это не должно сильно влиять на общую производительность.

Я протестировал его с компилятором Visual C++ 2013 x64 на Intel Core2 Duo E4700 (довольно старый, да). Массив размером 197 генерируется из элементов, предоставленных функцией rand(). Полный код со всеми тестами находится здесь. Вот время для выполнения 32 миллионов поисков:

[OP]
Time = 3.155 (-896368640) //the original OP's code
[Paul R]
Time = 2.933 (-896368640)
[stgatilov]
Time = 1.139 (-896368640) //the code suggested

Исходный код OP обрабатывает 10,6 миллионов массивов в секунду (2,1 миллиарда элементов в секунду). Предлагаемый код обрабатывает 29,5 млн массивов в секунду (5,8 млрд элементов в секунду). Кроме того, предложенный код хорошо работает для небольших массивов: даже для массивов из 15 элементов он почти в три раза быстрее исходного кода OP.

Вот сгенерированная сборка:

$LL56@main:
    movdqa  xmm2, xmm4
    movdqa  xmm0, xmm4
    movdqa  xmm1, xmm4
    lea rcx, QWORD PTR [rcx+64]
    pcmpgtd xmm0, XMMWORD PTR [rcx-80]
    pcmpgtd xmm2, XMMWORD PTR [rcx-96]
    pcmpgtd xmm1, XMMWORD PTR [rcx-48]
    paddd   xmm2, xmm0
    movdqa  xmm0, xmm4
    pcmpgtd xmm0, XMMWORD PTR [rcx-64]
    paddd   xmm1, xmm0
    paddd   xmm2, xmm1
    psubd   xmm3, xmm2
    dec r8
    jne SHORT $LL56@main
$LN54@main:
    phaddd  xmm3, xmm3
    inc rdx
    phaddd  xmm3, xmm3
    pextrw  eax, xmm3, 0

Напоследок хотелось бы отметить, что хорошо оптимизированный бинарный поиск можно сделать быстрее, переключившись на описанный векторизованный линейный поиск, как только интервал станет мал.

ОБНОВЛЕНИЕ. Дополнительную информацию можно найти на странице моя запись в блоге по этому поводу.

person stgatilov    schedule 20.07.2015

Если целевое решение является приемлемым, вы можете легко использовать SIMD (SSE, AltiVec или что-то еще, что у вас есть), чтобы получить ~ 4-кратное ускорение, тестируя 4 элемента за раз, а не только 1.

Из интереса я собрал простую реализацию SIMD следующим образом:

int linear_search_ref(const int32_t *A, int32_t key, int n)
{
    int result = -1;
    int i;

    for (i = 0; i < n; ++i)
    {
        if (A[i] >= key)
        {
            result = i;
            break;
        }
    }
    return result;
}

int linear_search(const int32_t *A, int32_t key, int n)
{
#define VEC_INT_ELEMS 4
#define BLOCK_SIZE (VEC_INT_ELEMS * 32)
    const __m128i vkey = _mm_set1_epi32(key);
    int vresult = -1;
    int result = -1;
    int i, j;

    for (i = 0; i <= n - BLOCK_SIZE; i += BLOCK_SIZE)
    {
        __m128i vmask0 = _mm_set1_epi32(-1);
        __m128i vmask1 = _mm_set1_epi32(-1);
        int mask0, mask1;

        for (j = 0; j < BLOCK_SIZE; j += VEC_INT_ELEMS * 2)
        {
            __m128i vA0 = _mm_load_si128(&A[i + j]);
            __m128i vA1 = _mm_load_si128(&A[i + j + VEC_INT_ELEMS]);
            __m128i vcmp0 = _mm_cmpgt_epi32(vkey, vA0);
            __m128i vcmp1 = _mm_cmpgt_epi32(vkey, vA1);
            vmask0 = _mm_and_si128(vmask0, vcmp0);
            vmask1 = _mm_and_si128(vmask1, vcmp1);
        }
        mask0 = _mm_movemask_epi8(vmask0);
        mask1 = _mm_movemask_epi8(vmask1);
        if ((mask0 & mask1) != 0xffff)
        {
            vresult = i;
            break;
        }
    }
    if (vresult > -1)
    {
        result = vresult + linear_search_ref(&A[vresult], key, BLOCK_SIZE);
    }
    else if (i < n)
    {
        result = i + linear_search_ref(&A[i], key, n - i);
    }
    return result;
#undef BLOCK_SIZE
#undef VEC_INT_ELEMS
}

На процессоре Core i7 с тактовой частотой 2,67 ГГц, используя OpenSUSE x86-64 и gcc 4.3.2, я получаю 7x - 8x улучшения в довольно широкой "зоне наилучшего восприятия", где n = 100 000, а ключ находится в средней точке массива (т. е. результат = н/2). Производительность падает примерно до 3.5x, когда n становится большим, и поэтому размер массива превышает размер кэша (предположительно, в этом случае становится ограниченной пропускная способность памяти). Производительность также падает, когда n мало, из-за неэффективности реализации SIMD (конечно, она была оптимизирована для больших n).

person Paul R    schedule 30.04.2010
comment
Вы можете использовать SIMD, но ускорение не будет в 4 раза, особенно для небольших массивов. Протестировано с SSE2 на Core i7. Я был бы заинтересован в вашей реализации. - person Mark Probst; 30.04.2010
comment
Для небольших массивов, возможно, нет, но для больших массивов, я думаю, вы должны иметь возможность увеличить 4x, используя SIMD. Я бы развернул основной цикл на 2, чтобы у вас было две векторные нагрузки, выдаваемые за итерацию, и тогда вы могли бы скрыть большую часть задержек. - person Paul R; 30.04.2010
comment
Я провел довольно много времени, возясь с этим, и лучшее ускорение, которое я могу получить с SSE2 по сравнению с моей лучшей реализацией без SSE2, составляет 2,6x для больших массивов. Я был бы рад проверить вашу реализацию, хотя :-) - person Mark Probst; 30.04.2010
comment
Для больших буферов примерно в 2,5 раза — это типичное ускорение, которое я видел для тщательно оптимизированного кода sse2 по сравнению с тщательно оптимизированной прямой математикой c. - person Alan; 30.04.2010
comment
@Alan: это во многом зависит от того, какой процессор вы используете, а также в некоторой степени от того, какой компилятор. До Woodcrest, когда SSE2 был только 64-битной реализацией под капотом, ускорение SSE было скромным по сравнению с полными 128-битными реализациями SIMD, такими как AltiVec, но начиная с Core 2 Duo вы сможете получить 4-кратное улучшение для float/int. . - person Paul R; 30.04.2010
comment
@Mark Probst: Хорошо, я добавил простую реализацию SIMD к моему ответу выше. Это в лучшем случае примерно в 8x быстрее, чем скалярный код, с размером массива порядка 100000 и значением ключа, которое находится на полпути через массив. Он падает примерно до 3.5x для очень больших массивов. - person Paul R; 01.05.2010
comment
Для небольших размеров массива это намного медленнее, чем мои самые быстрые реализации (и, на самом деле, медленнее, чем мои самые медленные). Для N=1000 это примерно так же быстро, как моя самая быстрая реализация без SIMD, но все же это даже не половина скорости моей лучшей реализации SSE2. При N=10000 это почти так же быстро, как моя лучшая реализация SSE2, но никогда не догоняет ее полностью. GCC 4.2.1 на Core i7. - person Mark Probst; 02.05.2010
comment
@Mark: Интересно, как вы его компилируете, а также как вы рассчитываете время? Я использую gcc -O3, и это исполняемый файл x86-64 (вдвое больше регистров SSE, чем x86). Когда я определяю время, я делаю это в цикле (100 итераций) и занимаю минимальное время — это означает, что для всех итераций, кроме первой, кэши будут загружены. Если вы измеряете только одну итерацию, ваши измерения будут искажены. И да, конечно, производительность для небольших массивов будет низкой — это ожидаемо, поскольку подпрограмма оценивает блоки массива, а не отдельные элементы или векторы. - person Paul R; 02.05.2010
comment
@Mark: еще одна проверка здравомыслия: какое у тебя абсолютное время? На частоте 2,67 ГГц я вижу около 1,0 нс/элемент при поиске скалярного кода и менее 0,15 нс/элемент при поиске моего SIMD-кода (оба для случая N = 100000, где ключ равен N/2, следовательно, время равно к общему времени / 50000). - person Paul R; 02.05.2010
comment
0,15 нс/искомый элемент означает, что вы выполняете 2,5 элемента/цикл. Мой код выполняет 2,6 элемента/цикл. Не стесняйтесь попробовать сами github.com/schani/linbin — я создал ветку paulr, которая включает ваша реализация. Протестируйте linear_sentinel_sse2_nobranch и linear_sse2_paulr. - person Mark Probst; 02.05.2010
comment
@Mark: Спасибо - у меня есть еще несколько идей для дальнейшей оптимизации, которые я опробую позже сегодня. Один включает использование SSE4, но я добавлю этот код #ifdef на случай, если вы захотите ограничить его SSE2. - person Paul R; 02.05.2010
comment
@Mark: ну, мои дальнейшие оптимизации не сильно помогли - я подозреваю, что производительность достигла точки, когда она ограничена пропускной способностью чтения кеша / памяти. Это всегда проблема, когда у вас очень мало вычислений по сравнению с вводом-выводом в память. - person Paul R; 02.05.2010
comment
Вполне может быть. Тогда хорошо для нас, не так ли? :-) - person Mark Probst; 02.05.2010

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

В качестве примера, в первой версии этого ответа я предположил, что на 100-200 элементов массива немного более высокие накладные расходы бинарного поиска должны легко быть оплачены гораздо меньшим количеством зондов в массиве. Однако в комментариях ниже Марк Пробст сообщает, что он видит линейный поиск вперед примерно до 500 записей на своем оборудовании. Это усиливает необходимость измерения при поиске наилучшей производительности.

Примечание: отредактировано после комментариев Марка ниже о его измерениях линейного и бинарного поиска для достаточно малых значений N.

person Dale Hagglund    schedule 30.04.2010
comment
Мой лучший линейный поиск превосходит стандартный бинарный поиск до N=550 на Core i7. - person Mark Probst; 30.04.2010
comment
Спасибо за информацию. Я обновил свой комментарий, чтобы отразить это. - person Dale Hagglund; 30.04.2010
comment
Общие правила оптимизации: 1) Не делайте, 2) Измеряйте Учитывая, что все это было мысленным упражнением, № 1 не применяется. Но № 2 всегда должен применяться. Я рад, что кто-то поднял это! - person Harold Bamford; 30.04.2010

Можно параллельно.

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

person fortran    schedule 30.04.2010
comment
Практически невозможно, чтобы создание даже одного потока было дешевле, чем линейное сканирование 100-200 элементов. - person Dale Hagglund; 30.04.2010
comment
Тем не менее, если будет много поисков, их можно выполнять параллельно, а потоки можно объединить в пул и использовать повторно. - person fortran; 30.04.2010
comment
В этом случае, если вы ищете ‹60 товаров, нет необходимости делать это параллельно. Однако есть несколько вариантов использования (у меня есть один сейчас), когда массив элементов не упорядочен и порядок не может быть изменен. В этом случае нельзя использовать двоичный поиск, и если размер массива довольно велик (он должен быть где-то около 10 000, чтобы оправдать дополнительные усилия), разделение массива и параллельный поиск определенно были бы жизнеспособным решением. - person Richard; 09.09.2010
comment
Да, для больших массивов вы можете себе представить, что разные части массива могут оставаться горячими в частном кеше L2 на разных ядрах. Для массива из 64 элементов затраты на синхронизацию при отправке поиска в рабочий поток выше, чем просто выполнение этого в потоке, которому нужен результат. - person Peter Cordes; 21.09.2016

Если вы используете платформу Intel:

int linear (const int *array, int n, int key)
{
  __asm
  {
    mov edi,array
    mov ecx,n
    mov eax,key
    repne scasd
    mov eax,-1
    jne end
    mov eax,n
    sub eax,ecx
    dec eax
end:
  }
}

но это находит только точные совпадения, а не совпадения больше или равные.

В C вы также можете использовать устройство Дафф:

int linear (const int *array, int n, int key)
{
  const int
    *end = &array [n];

  int
    result = 0;

  switch (n % 8)
  {
    do {
  case 0:
    if (*(array++) >= key) break;
    ++result;
  case 7:
    if (*(array++) >= key) break;
    ++result;
  case 6:
    if (*(array++) >= key) break;
    ++result;
  case 5:
    if (*(array++) >= key) break;
    ++result;
  case 4:
    if (*(array++) >= key) break;
    ++result;
  case 3:
    if (*(array++) >= key) break;
    ++result;
  case 2:
    if (*(array++) >= key) break;
    ++result;
  case 1:
    if (*(array++) >= key) break;
    ++result;
    } while(array < end);
  }

  return result;
}
person Skizz    schedule 30.04.2010
comment
Будьте осторожны, рекомендуя устройство Даффа. Это умный код C, в какой-то степени умный, но поскольку он чрезвычайно неструктурирован, он иногда может победить современные оптимизирующие компиляторы. - person Dale Hagglund; 30.04.2010
comment
@Dale: Вы правы, современные компиляторы почти наверняка справятся с развертыванием цикла лучше, чем это. - person Skizz; 01.05.2010
comment
repne scasd имеет значительные накладные расходы при запуске и даже не так быстр по сравнению с SIMD. (rep stos и rep movs хороши (особенно для больших блоков, чтобы амортизировать их накладные расходы при запуске) и внутренне работают в 16-байтовых или 32-байтовых фрагментах, но, насколько мне известно, инструкции условной строки повторения (scas и cmps) не намного больше, чем скалярный цикл, реализованный в микрокоде.) См. таблицы insn Agner Fog и руководство по оптимизации сборки, а также другие ссылки в вики тегов x86, например руководство по оптимизации Intel. - person Peter Cordes; 21.09.2016
comment
Обновление по этому поводу: repne scasd не поддерживает Fast Strings ни на одном из существующих процессоров. В лучшем случае он сравнивает 1 DWORD за такт после запуска, даже на последних процессорах Skylake / Ryzen. В 2010 году, когда был опубликован этот ответ, Nehalem был актуальным и мог выполнять одну 16-байтовую загрузку SIMD за такт. Intel, начиная с Haswell, и AMD, начиная с Zen2, могут выполнять 2 загрузки по 32 байта за такт, а SIMD ALU работает для сравнения и проверки ключа. (Или стгатиловская версия без веток просто засчитывается, чтобы найти, где был ключ). Придется понизить это: это не оптимально ни для чего, кроме, возможно, размера кода. - person Peter Cordes; 08.08.2019

Если бы у вас был квантовый компьютер, вы могли бы использовать алгоритм Гровера для поиска ваших данных в O (N1/2) раз и используя O(log N) дискового пространства. В противном случае ваш вопрос довольно глуп. Бинарный поиск или один из его вариантов (например, троичный поиск) — действительно лучший выбор. Выполнять микрооптимизацию линейного поиска глупо, когда вы можете выбрать более совершенный алгоритм.

person George    schedule 30.04.2010
comment
Хорошо, мистер Умник, у меня есть Core i7, и мне нужно выполнить поиск в массиве размером 64, и он должен быть сверхбыстрым. Линейный или двоичный? Любые дальнейшие оптимизации? - person Mark Probst; 30.04.2010
comment
Джордж: Для небольших массивов промахи кэша и неправильные предсказания переходов будут преобладать во времени выполнения бинарного поиска. Линейный поиск может использовать предварительную выборку для устранения промахов в кэше и сможет предсказать большинство переходов. - person Gabe; 30.04.2010
comment
Да, вы можете сделать почти все за константное время, если вы просто сделаете константу достаточно большой. Но вопрос был не в этом. - person Mark Probst; 30.04.2010
comment
Теоретически поиск в массиве фиксированного размера выполняется за постоянное время. Теоретически разницы между теорией и практикой нет. На практике это не так. - person Alan; 30.04.2010
comment
@Mark: Верно, но если вы ищете массив размером 64, зачем прилагать все эти усилия для оптимизации поиска? - person BlueRaja - Danny Pflughoeft; 01.05.2010
comment
Я мог бы задать тот же вопрос для любого размера массива, не так ли? - person Mark Probst; 02.05.2010

Я знаю, что эта тема старая, но я не мог удержаться от сообщения. Моя оптимизация для дозорного линейного поиска:

int sentinel_linear_search(int key, int *arr, int n)
{
    int last_value, i;

    /* considering that n is the real size of the array */
    if (--n < 1)
        return -1;

    last_value = arr[n];

    /* set array last member as the key */
    arr[n] = key;

    i = 0;
    while (arr[i] != key)
        ++i;

    /* recover the real array last member */
    arr[n] = last_value;

    return (arr[i] == key) ? i : -1;
}

Большим улучшением дозорного поиска является то, что его итерация использует только одну условную ветвь (ключ) вместо двух (индекс и ключ).

    while (arr[i] != key)
        ++i;
person Geyslan G. Bem    schedule 28.11.2015
comment
После комментария usr я удалил переменную ret и сократил код. спасибо - person Geyslan G. Bem; 28.11.2015

развернуть с фиксированными индексами массива.

int linear( const int *array, int n, int key ) {
  int i = 0;
  if ( array[n-1] >= key ) {
     do {
       if ( array[0] >= key ) return i+0;
       if ( array[1] >= key ) return i+1;
       if ( array[2] >= key ) return i+2;
       if ( array[3] >= key ) return i+3;
       array += 4;
       i += 4;
     } while ( true );
  }
  return -1;
}
person drawnonward    schedule 30.04.2010

Этот ответ немного более неясен, чем мой другой, поэтому я публикую его отдельно. Он основан на том факте, что C гарантирует логический результат false=0 и true=1. X86 может создавать логические значения без ветвления, поэтому он может быть быстрее, но я его не тестировал. Подобные микрооптимизации всегда будут сильно зависеть от вашего процессора и компилятора.

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

Чтобы определить оптимальную величину развертывания цикла, нужно поэкспериментировать. Вы хотите найти точку убывающей (или отрицательной) отдачи. Я собираюсь взять SWAG и попробовать 8 на этот раз.

static int
linear (const int *arr, int n, int key)
{
        assert(arr[n] >= key);
        int i = 0;
        while (arr[i] < key) {
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
                i += (arr[i] < key);
       }
       return i;
}

Редактировать. Как указывает Марк, эта функция вводит зависимость в каждой строке от предыдущей строки, что ограничивает способность конвейера процессора выполнять операции параллельно. Итак, давайте попробуем небольшую модификацию функции, чтобы удалить зависимость. Теперь функция действительно требует 8 дозорных элементов в конце.

static int 
linear (const int *arr, int n, int key) 
{ 
        assert(arr[n] >= key);
        assert(arr[n+7] >= key);
        int i = 0; 
        while (arr[i] < key) {
                int j = i;
                i += (arr[j] < key); 
                i += (arr[j+1] < key); 
                i += (arr[j+2] < key); 
                i += (arr[j+3] < key); 
                i += (arr[j+4] < key); 
                i += (arr[j+5] < key); 
                i += (arr[j+6] < key); 
                i += (arr[j+7] < key); 
       } 
       return i; 
} 
person Mark Ransom    schedule 30.04.2010
comment
Хороший, но я не думаю, что он будет работать хорошо, потому что он вводит зависимость данных для индекса i, чего нет в более простом линейном поиске. Я проверю это. Кроме того, вам нужно 8 дозорных значений, а не только одно. - person Mark Probst; 30.04.2010
comment
Данные есть - работает ужасно :-). Он превосходит даже простой, не сторожевой, не развернутый линейный поиск почти в 2 раза. - person Mark Probst; 30.04.2010
comment
О, хорошо, это стоило того. И вам по-прежнему нужен только один дозорный, потому что индекс перестает увеличиваться, как только вы его достигаете. - person Mark Ransom; 30.04.2010
comment
@Марк Пробст, попробуй мою новейшую морщинку. - person Mark Ransom; 30.04.2010
comment
Намного лучше. Примерно на 30 % быстрее, чем стандартный линейный поиск болота, но все же только примерно в два раза быстрее развернутого линейного поиска с часовым. Мой код теперь находится в сети по адресу github.com/schani/linbin — не стесняйтесь экспериментировать с ним. . - person Mark Probst; 30.04.2010

Вы можете избежать n проверок, подобных тому, как это делает развертывание цикла.

static int linear(const int *array, int arraySize, int key)
{
  //assuming the actual size of the array is always 1 less than arraySize
  array[arraySize] = key; 

  int i = 0;
  for (; ; ++i)
  {
     if (array[i] == key) return i;
  }
}
person archon    schedule 13.05.2011
comment
Если нет элемента, похожего на ключ, вы будете читать за пределами. Чтобы использовать одно условие перехода, необходимо установить последний (или первый, если инверсно) элемент массива. См. мой ответ: stackoverflow.com/a/33972674/2776344 - person Geyslan G. Bem; 28.11.2015

петля назад, это может быть переведено ...

// loop backward

for (int i = arraySize - 1; i >=0; --i)

... к этому ("может быть" быстрее):

    mov cx, arraySize - 1
detectionHere:
    ...
    loop detectionHere   

Кроме того, только бинарный поиск может ускорить поиск.

person Michael Buen    schedule 30.04.2010
comment
loop не быстрый; самые сложные инструкции в настоящее время выполняются медленнее, чем несколько простых инструкций. Кроме того, не будет ли это плохо использовать кеши? - person Bastien Léonard; 30.04.2010
comment
следовательно, может быть быстрее. одной инструкцией меньше, одним циклом меньше, просто мои мысли - person Michael Buen; 30.04.2010

это может вызвать векторные инструкции (предложенные Gman):

for (int i = 0; i < N; i += 4) {
    bool found = false;   
    found |= (array[i+0] >= key);
    ...
    found |= ( array[i+3] >= key);
    // slight variation would be to use max intrinsic
    if (found) return i;
}
...
// quick search among four elements

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

еще одна вещь, которая может помочь векторизации (выполнение вертикального максимального сравнения):

for (int i = 0; i < N; i += 8) {
    bool found = false;   
    found |= max(array[i+0], array[i+4]) >= key;
    ...
    found |= max(array[i+3], array[i+7] >= key;
    if (found) return i;
}
// have to search eight elements
person Anycorn    schedule 30.04.2010
comment
По сути, @the_drow вы надеетесь использовать векторные инструкции, чтобы делать в 4 раза больше вещей за один раз. многие компиляторы могут быть вынуждены использовать такие инструкции. во-первых, вы загружаете 4 элемента, во-вторых, вы загружаете восемь элементов и удаляете половину с помощью векторной функции max. результатом является диапазон, в котором находится индекс (длиной четыре или восемь элементов). после этого вам нужно искать точный индекс в небольшом диапазоне. - person Anycorn; 30.04.2010

Вы можете искать более крупный элемент, чем int за раз - в частности, платформа, это может быть намного быстрее или медленнее в зависимости от того, как он обрабатывает чтение больших данных. Например, в 64-разрядной системе чтение массива по 2 элемента за раз и проверка элементов hi/low по отдельности может выполняться быстрее из-за меньшего количества операций ввода-вывода. Тем не менее, это сортировка O (n), несмотря ни на что.

person Michael Dorgan    schedule 30.04.2010

В одном из комментариев вы сказали, что длина массива равна 64.

Что ж, если вы должны сделать это линейно, вы можете сделать следующее:

int i = -1;
do {
  if (arr[0] >= key){i = 0; break;}
  if (arr[1] >= key){i = 1; break;}
  ...
  if (arr[62] >= key){i = 62; break;}
  if (arr[63] >= key){i = 63; break;}
} while(0);

Однако я серьезно сомневаюсь, что это быстрее, чем этот бинарный поиск: *

int i = 0;
if (key >= arr[i+32]) i += 32;
if (key >= arr[i+16]) i += 16;
if (key >= arr[i+ 8]) i +=  8;
if (key >= arr[i+ 4]) i +=  4;
if (key >= arr[i+ 2]) i +=  2;
if (key >= arr[i+ 1]) i +=  1;

*Спасибо Джону Бентли за это.

Добавлено: поскольку вы сказали, что эта таблица подготовлена ​​один раз для большого количества поисков, и вы хотите быстро, вы можете выделить где-то немного места и сгенерировать машинный код с жестко зашитыми в него значениями. Это может быть линейный или бинарный поиск. В случае бинарного кода машинный код будет выглядеть так, как это сгенерирует компилятор:

if (key < value32){
  if (key < value16){
    ...
  }
  else {
    ...
  }
}
else {
  if (key < value48){
    ...
  }
  else {
    ...
  }
}

Затем вы просто копируете это в то место, где вы можете его вызвать.

ИЛИ вы можете распечатать приведенный выше код, скомпилировать и скомпоновать его на лету в dll и загрузить dll.

person Mike Dunlavey    schedule 30.04.2010

На самом деле ответ на этот вопрос на 100% зависит от платформы, для которой вы пишете код. Например:

CPU : Memory speed | Example CPU | Type of optimisation
========================================================================
    Equal          |    8086     | (1) Loop unrolling
------------------------------------------------------------------------
  CPU > RAM        |  Pentium    | (2) None
  1. Избегание условного перехода, необходимого для зацикливания данных, даст небольшое улучшение производительности.
  2. Как только ЦП начинает работать быстрее, чем ОЗУ, не имеет значения, насколько эффективным становится цикл (если только это не действительно плохой цикл), он будет останавливаться из-за необходимости ждать, пока данные будут загружены из ОЗУ. SIMD на самом деле не помогает, поскольку преимущество параллельного тестирования все еще перевешивается необходимостью ждать поступления дополнительных данных. SIMD действительно вступает в свои права, когда вы ограничены процессором.
person Skizz    schedule 30.04.2010
comment
Данные (schani.wordpress.com/2010/04/ 30/linear-vs-binary-search) не согласуется с вашей теорией реальности. - person Mark Probst; 01.05.2010
comment
@Mark: Ваш метод, по-видимому, устраняет накладные расходы на ОЗУ, отбрасывая два самых медленных времени, поэтому вы эффективно измеряете алгоритм, а не всю систему. После пары прогонов массив будет загружен в кэш L1 и L2, и доступ к нему будет достаточно быстрым. Было бы интересно увидеть два самых медленных времени, включенных в тайминги - если бы вы могли гарантировать, что данные находятся в ОЗУ, а не в кеше, тогда алгоритм оказывал бы меньшее влияние на тайминги. - person Skizz; 02.05.2010
comment
Я не отбрасываю два самых медленных времени индивидуального поиска — я не могу рассчитать время поиска, который занимает всего несколько циклов. Я делаю, скажем, те же самые 20 миллионов случайных поисков, 10 раз, и отбрасываю время для двух самых медленных и двух самых быстрых из этих 10 запусков. Я усредняю ​​6 оставшихся и делю среднее время на 20 миллионов, чтобы получить среднее время для одного отдельного поиска. Если вы знаете, как надежно определить время такого поиска из оперативной памяти, т.е. с пустыми кэшами L2 и L3, пожалуйста, дайте мне знать. - person Mark Probst; 02.05.2010
comment
На четырехъядерном i7 одно ядро ​​может почти полностью заполнить пропускную способность DRAM. На типичном Haswell или Skylake это около 8 байт на такт ядра, так что да, вам нужен SIMD, чтобы не отставать даже от DRAM, не говоря уже о кэше L3. В программе, в которой стоит оптимизировать этот поиск, она, вероятно, работает достаточно, чтобы оставаться горячей, по крайней мере, в L3, возможно, в L2. Более широкий SIMD означает больше работы при меньшем количестве операций, поэтому он помогает удерживать больше промахов кэша в полете (одно и то же окно с нарушением порядка может видеть больше байтов впереди, чтобы раньше запускать обходы страниц и промахи кэша; предварительная выборка данных HW обычно останавливается на границах 4 КБ.) - person Peter Cordes; 08.08.2019
comment
Я думаю, что люди неправильно поняли мой ответ. Для линейного поиска алгоритм ограничен скоростью, с которой данные могут быть извлечены из ОЗУ (или диска для действительно больших массивов), как только вы достигнете пиковой скорости передачи данных, улучшение алгоритма мало повлияет на общую скорость. Да, изменение алгоритма может ускорить его за счет уменьшения объема данных, перемещаемых через систему, но в вопросе говорилось только о линейном поиске. - person Skizz; 15.08.2019

Ну, вы могли бы использовать указатели...

static int linear(const int *array, int arraySize, int key) {
    int i;

    for(i = 0; i < arraySize; ++i) {
        if(*array >= key) {
            return i;
        }

        ++array;
    }

    return arraySize;
}
person strager    schedule 30.04.2010
comment
@the_drow: Теоретически увеличение указателя, а затем тестирование должно быть быстрее, чем: увеличение счетчика, затем добавление его к адресу, а затем тестирование. На практике я сомневаюсь, что они разные. Стилистически я предпочитаю ответ, представленный здесь, потому что он устраняет догадки (и лично я думаю, что он звучит лучше). - person GManNickG; 30.04.2010
comment
Да, но компилятор, вероятно, все равно оптимизирует этот бит. Вы также можете попробовать развернуть цикл. - person BobbyShaftoe; 30.04.2010
comment
Посмотрите на вывод вашего компилятора, он, вероятно, такой же, как код OP. (gcc выполняет эту оптимизацию начиная с ‹2.95, где я впервые заметил это.) Переменная счетчика будет инициализирована значением n, и каждый раз в цикле счетчик будет уменьшаться, в то время как указатель будет продвигаться вперед на 4 (или на любой другой sizeof( int) возвращается). - person dash-tom-bang; 30.04.2010
comment
Я не думаю, что это вообще помогает. Это просто означает, что вы увеличиваете дополнительную переменную в каждом цикле. Если разыменование указателя выполняется быстрее, чем array[i]... - person Chris Cooper; 30.04.2010
comment
@ Шафто, да; такого рода микрооптимизацию мне сложно делать с чистой совестью. - person strager; 30.04.2010
comment
@GMan: Практически любой компилятор, предлагающий оптимизацию кода, уменьшит счетчик счетчика + индекс массива до арифметики указателя в сгенерированном коде. - person dthorpe; 30.04.2010
comment
@Chris: Вы читали подробности того, что я написал? Дополнительная переменная не увеличивается, увеличение n было заменено увеличением указателя, и математика индексации исчезает. @dthrope: Действительно, поэтому я и сказал На практике... - person GManNickG; 30.04.2010
comment
В прошлый раз, когда я тестировал на X86, array[i] был быстрее, чем *array, потому что вам нужно было сделать только одно приращение вместо двух. - person Mark Ransom; 30.04.2010
comment
Это в худшем случае сделает ваш код медленнее, потому что вы делаете одно дополнительное приращение за итерацию. Если компилятор умен, он даст ту же производительность, что и исходный код. - person Mark Probst; 30.04.2010
comment
@GMan - код изменился с тех пор, как я оставил свой комментарий, поэтому он больше не применим. - person Chris Cooper; 30.04.2010
comment
@Chris: код вообще не изменился. Нет истории редактирования, и ваш комментарий был сделан через 6 минут после публикации ответа (так что никаких бесплатных правок). - person GManNickG; 30.04.2010
comment
@GMan - Если только мне не потребовалось некоторое время, чтобы написать комментарий, который я полагаю ...? Во всяком случае, другие люди говорили подобные вещи. Я перечитал пост, и мой комментарий все еще применим. Но я согласен с Роменом... Здесь слишком много мудрецов. =П - person Chris Cooper; 30.04.2010
comment
@Chris: Ну, у меня также есть личное свидетельство о том, что код не изменился, но я предпочитаю утверждения, основанные на доказательствах. В любом случае, этот ответ справедлив, хотя явно не то, что ищет ОП. (Какой такой плохо сформулированный вопрос, Стагер дает честный ответ.) - person GManNickG; 30.04.2010
comment
@GMan - Согласен. И независимо от того, изменилось оно или нет, изменение, о котором я свидетельствую, было просто перемещением массива ++ из цикла for() в конец цикла, который не имеет никакой функциональной разницы. =П - person Chris Cooper; 30.04.2010
comment
Если вы хотите помочь компилятору оптимизировать отдельное целое число внутри цикла, удалите переменную i. Используйте return p - array для вычисления длины из вычитания указателя, если вы действительно хотите, чтобы компилятор сделал более плотный внутренний цикл. Если вы не покажете вывод компилятора, показывающий, что это делает внутренний цикл лучше, даже если у вас все еще есть i++, а также array++. - person Peter Cordes; 08.08.2019