Недавно узнал, что сервис проверки российских паспортов на недействительность является очень популярным. 😊
Самым актуальным вариантом является сервис МВД, но он закрыт капчей, что не всегда бывает удобно пользователям. Поэтому многие организации скачивают с сайта МВД файл с данными и работают уже с ним.
Традиционно программисты, не мудрствуя лукаво, просто сохраняют данные о паспортах в БД, пользуясь тем SQL-сервером, который используют для основного проекта.
Тут правда, возникает проблемка. Дело в том, что данные о недействительных паспортах достаточно часто обновляются (в среднем пару раз в неделю), а загрузка их в БД занимает время, причем достаточно значительное. Ну просто потому, что сейчас в России имеется более 110 млн. недействительных паспортов.
Меня этот момент заинтересовал, особенно если вспомнить, какой курс я читал, занимаясь преподаванием в СибГИУ.😉
Естественно, я захотел попробовать, с учетом особенности задачи, реализовать ее более оптимально. Идея тут очень простая: объем данных хоть и большой, но оперативная память сейчас относительно дешевая. Поэтому надо попытаться воспользоваться этой особенностью.
Нужно просто выбрать подходящие для этой задачи структуры данных и их реализовать. Впрочем, по большому счету, особо размышлять над этим вопросом мне не пришлось, так как в одной из самых первых ссылок на тему недействительных паспортов все уже было разложено по полочкам.
Тем не менее, я все же поэкспериментировал с различными вариантами, опишу просто более подробно обоснование выбора структур данных.
Одна из самых быстрых структур для поиска (да и добавления) данных является хэш-таблицы. Правда, только в том случае, когда ключевое поле данных имеет небольшой размер. Недостатком же хэш-таблиц является коллизии, а также связанная с этим некоторая избыточность памяти. Когда память очень хочется сэкономить (а это именно наш случай), то хэш-таблицы могут быть не самым подходящим вариантом.
Идея использовать в качестве ключа серию и номер паспорта как единое число не очень хороша. Во-первых, потому что ключ получается слишком длинный, а во-вторых, возникает проблема коллизий.
Серия и номер паспорта можно сохранить как пятизначное беззнаковое целое число. Но такого типа среди напрямую поддерживаемых современными процессорами нет. Поэтому либо придется городить свой тип, что чревато некоторой потерей производительности. Либо использовать стандартное восьмибайтовое целое, но при этом иметь значительный перерасход памяти.
Еще больше усугубит проблему памяти необходимость разрешения коллизий, так как выделить массив, покрывающий все пространство возможных значений ключей нереально. Он потребует под себя 5e10 байт, или 46 Гбайт ОЗУ, что несколько многовато для такой простой задачи.
При использовании меньшего размера хэш-таблицы потребуется место для разрешения коллизий. В случае использовании открытой адресации для сохранения приемлемой скорости работы потребуется размер хэш-таблицы примерно на 25% больший количества ключей. Таким образом, потребуется примерно 5*110e6*1.25 байт, или 655 Мбайт ОЗУ. Однако если сформируется достаточно длинная очередь проб для какого-либо подмножества ключей (а скорее всего так и произойдет), то в некоторых случаях производительность будет не очень высокой.
В случае использования закрытой адресации можно ограничиться хэш-таблицей размером в два раза меньше общего количества элементов, но потребуется дополнительная память для связи элементов в линейный список. Таким образом, общий объем памяти можно оценить на текущий момент так: 8*55e6+5*110e6 байт или 944 Мбайт ОЗУ. Т. е. закрытая адресация потребует больше памяти, но даст в среднем несколько более высокую производительность. А нам надо стараться экономить память, особенно в случае закрытой адресации. Во-первых, на вырост, а во-вторых, что бы как можно дольше оставаться в рамках 32-разрядной адресации.
Лирическое отступление. Чем плоха 64-разрядная адресация? Тем, что при активном использовании указателей, на их непродуктивное хранение используется в два раза больше памяти, чем в 32-разрядных системах. Например, если хэш-таблицу с закрытой адресацией перенести в 64-разрядную среду, то на хранение тех же самых данных потребуется уже 16*55e6+5*110e6 байт или 1363 Мбайта ОЗУ. Активное использование длинных указателей также дополнительно загружает шину данных, что немножко снижает производительность.
Поэтому, если объем обрабатываемых данных не превышает 2 Гбайт, то лучше оставаться в рамках 32-разрядного приложения.
Почему 2 Гбайт, а не 4? Этот вопрос лучше задать архитекторам Microsoft. Думаю, ответ имеет ту же природу, что и стародавнее утверждение, что 640 Кбайт хватит всем на всю оставшуюся жизнь.
Кроме варианта использованием одного ключа из серии и номера паспорта, можно использовать два. Сначала, например, ищем по серии, затем по номеру, или наоборот. Давайте посмотрим, к чему это приведет.
Главное, что в этом случае легко получить идеальную хэш-таблицу, в которой совсем нет коллизий, а поэтому практически не требуется лишняя память.
Номер паспорта отлично укладывается в массив [0..999999], а серия - в массив [0..9999].
Какой же из вариантов взять в качестве первого ключа? Для этого надо понять, какую структуру данных использовать в качестве второго ключа. Очевидно, что обычная хэш-таблица тут будет не очень хороша, так потребует слишком много памяти для реализации. Поэтому в первом приближении лучше использовать обычный сортированный массив, для ускорения операции поиска по вторичному ключу.
Посчитаем требуемый объем памяти. Если в качестве первого ключа взять серию, то объем памяти составит 9999*(4+const)+N*4 (можно ужаться до 3 байт на номер, но сложнее будет реализация, далее увидим, что и так получается неплохо), где const - затраты на организацию 2 ключа, а N - количество паспортов.
Если же в качестве первого ключа использовать номер паспорта, то требуемый объем памяти составит 999999*(4+const)+N*2.
Так как N много больше, чем 999999, то очевидно, что второй вариант гораздо экономичнее использует память. Точнее, при N > (4+const)*495000 будет выгоднее использовать второй вариант. Так как const - число не большее, вряд ли превышающее 20 байт, то второй вариант будет предпочтительным уже при 12 млн. паспортов.
Если рассчитать объем требуемой памяти, то окажется, что для хранения и поиска 110 млн. паспортов потребуется всего 232 Мбайта ОЗУ. В реальности памяти требуется раза так в 2 больше, из-за гранулярности виртуальной памяти и выравнивая границ хранящихся там элементов.
По мере увеличения количества недействительных паспортов, более оптимальной может стать другая структура для второго ключа. Так как нам нужно знать только факт наличия серии для заданного номера, то список серий можно хранить в виде битового массива. Тогда проверка наличии серии будет занимать всего 1 операцию, то есть будет значительно быстрее.
В этом случае для хранения информации уже о всех возможных паспортах потребуется 999999*(4+const+9999/8) или 1215 Мбайт. То есть 32-разрядной системы хватит для заданного формата паспортов навсегда.
Теперь о результатах. На тестах на бюджетном, и, теперь уже достаточно стареньком, процессоре AMD выдает примерно 3 млн. операций поиска что при нахождении паспорта в списке, что при его отсутствии. Загрузка новой информации занимает меньше 5 минут. Думаю, что не один SQL сервер не сможет выдать подобную производительность в таких же условиях.
Для интереса сделал веб-интерфейс к своей системе, хоть подобных сайтов сейчас полным-полно.
А вот если в качестве номера и/или серии паспорта будет использоваться не числа, а строки, то от использования такой организации данных придется отказаться. Это приведет как к увеличению объема требуемой памяти, так и существенному замедлению работы.
Спасибо за очередное интересное исследование! На часах 12 ночи, а я все пробую разные структуры данных :)
ОтветитьУдалитьЖелезо: i7-4870HQ @ 2.50GHz
Тесты писал на Java, при загрузки сразу превращал серию+номер в long (пропуская буквенные серии, например "3ЕР5,86638"). Загрузка упирается в разархивирование bz2 архива, если из распакованного csv то в зависимости от алгоритма ~30-70 сек.
Теперь самое интересное, что я проверил (от самого быстрого на чтение к самому медленному):
1. Положить long как есть в хеш таблицу с открытой адресацией, самым простым линейным разрешением коллизий, 75% максимальным заполнением и увеличением сразу в 2 раза при нехватке места. Заняла она 2 Гб, заполнение 40%. Скорость на чтение - 18 MOPS!
2. Легко заметить что нам чуть-чуть не хватает int :(. Как и в Вашем варианте используем 2 ключа, но: 2-й: младшие 4 байта, а 1-й - все что осталось (по текущим данным это всего 3 значения: 0, 1, 2). Для 1-го ключа: стандартный Java HashMap (закрытая адресация), для 2-го: хэш таблицы из 1-го эксперимента (но с int). Памяти: 800 МБ (52% заполнение), скорость: 11 MOPS.
3. 2-й эксперимент, но более компактно размещаем данные увеличивая таблицу только при 70% загрузке и всего на 20%, используем квадратичное пробирование для разрешения коллизий. Память: 500 МБ (85% заполнение), скорость 7.7 MOPS.
4. Массив из 1М "быстрых" хэш таблиц с открытой адресацией. Память: 487 МБ на данные (44% заполнение из short-ов) + ??? на 1М Java объектов, скорость: 5.4 MOPS
5. 4-й эксперимент, но используем "компактные" хэш таблицы. Память: 266 МБ (80% заполнение) + ??? на 1М Java объектов, скорость: 3.4 MOPS
6. 2-й эксперимент, но используем сортированный массив int-ов для 2-го ключа. Память: 430 МБ, скорость 1.5 MOPS. Здесь все совсем печально, случайное гуляние по огромному массиву совсем не нравится процессору.
Если я правильно понял, то у Вас что-то похоже на мои 4 и 5 эксперименты? Скорость вроде тоже близкая, правда CPU другой.
Результаты вполне ожидаемы: хочешь супер быстро давай много памяти. Там где нужна скорость 50% хэш таблице с открытой адресацией и линейным разрешением коллизий похоже нет равных. Уж сильно она CPU-cache friendly :)
PS. Используемая хэш функция: https://github.com/vigna/fastutil/blob/master/src/it/unimi/dsi/fastutil/HashCommon.java#L106
PS2. Ничего не профилировал, возможно где-то сильно накосячил.
Очень быстро! :)
ОтветитьУдалитьПроцессоры разные, возможно, дело в этом. Твой, подозреваю, не менее чем в 2 раза быстрее моего. AMD FX-4350.
С п. 1 не экспериментировал особо, так как экономил память. Интересно, как ты хранил информацию о наличии цепочки? Что бы при поиске в случае неудачной попытки сразу знать, искать ли дальше.
2. В этом случае, может быть, проще использовать массив, состоящий из трех элементов вместо HashMap?
Я думаю, у нас полное совпадение с твоим шестым экспериментом. На текущий момент у одного номера паспорта не более 300 серий, кмк. Надо, кстати, проверить. Соответственно, в худшем случае не более 9 итераций двоичного поиска. Не так уж и много, на самом деле. Ну и заметно, что в этом варианте Java проигрывает по производительности статическому компилятору.
Кстати, Java в последних версиях умеет компилиться при необходимости в нативный код процессора?
> Процессоры разные, возможно, дело в этом. Твой, подозреваю, не менее чем в 2 раза быстрее моего. AMD FX-4350.
УдалитьДа вот не знаю... Ваш как я понял десктопный с 4.2GHz базовой частотой, а мой мобильный всего на 2.5GHz. Как я понимаю основной прогресс сейчас идем в векторных инструкциях и числе ядер, а тут не особо видно где они могут пригодиться.
> Интересно, как ты хранил информацию о наличии цепочки?
Я писал не сам, а использовал http://fastutil.di.unimi.it :) Вот здесь можно посмотреть реализацию: http://grepcode.com/file/repo1.maven.org/maven2/it.unimi.dsi/fastutil/7.0.6/it/unimi/dsi/fastutil/longs/LongOpenHashSet.java (методы add & contains). Если коротко, то никак не хранил. При поиске просто перебираем подряд пока не найдем искомое значение или не уткнемся в нуль (пусто), а для сохранения нуля используем отдельный boolean.
> может быть, проще использовать массив, состоящий из трех элементов вместо HashMap?
Да конечно, просто лень было переделывать.
> Я думаю, у нас полное совпадение с твоим шестым экспериментом.
Перечитал еще раз Ваш пост и как я понял эксперименты все таки разные... У Вас 1М массив из крохотных сортированных short массивов, а у меня 3 ~50M int массива.
Протестировал массив из 1М массивов отсортированных short. Скорость: ~2.8 MOPS. Вроде это как раз то что Вы реализовали :) и скорость очень близкая.
> худшем случае не более 9 итераций двоичного поиска.
Все так, но как я уже писал, память она жутко медленная, а тут почти наверняка процессор не предскажет правильно адрес и не загрузит данные заранее. Пару обращений к памяти и все скорость просела.
> Кстати, Java в последних версиях умеет компилиться при необходимости в нативный код процессора?
Уже лет 10 наверное и довольно с крутыми оптимизациями, Java уже давно не медленная.
Мне все еще нечем заняться :)
Удалить1. Как Вы и предложили переделал 2-й вариант на крохотный массив из хэш таблиц. Скорость сравнялась с одной long хэш таблицей. В целом ожидаемо, массив горячий и постоянно в кэше на скорость почти не влияет, но память экономится.
2. Я облажался с запросами, ~ в 80% спрашивал существующий элемент, что нечестно по отношению к бинарному поиску...
Переделал, новые цифры, 3 скорости: все элементы существуют, 50/50, все НЕ существуют:
1. Массив из 3-х int хэш таблиц:
- 37% заполнение, 1152Mб: 29, 20, 23 MOPS (почему-то 50/50 медленнее чем 100% промах, возможно просто процессор был чем-то еще занят)
- 50% заполнение, 872Мб: 22, 13, 12 MOPS
- 85% заполнение, 502Мб: 16, 8.1, 6.2 MOPS
2. Массив из 1M short хэш таблиц:
- 44% заполнение, 487Mб + 1М * ???: 5.8, 4.7, 5.1 MOPS
- 80% заполнение, 266Мб + 1M * ???: 4.3, 2.5, 2.4 MOPS
3. Массив из 1М отсортированных short массивов:
- ~ 246Mб: 4.2 MOPS
В целом выводы вроде все те же: хэш таблицы быстрые, но любят кушать память...
У твоего процессора в турбо-режиме частота 3.7 ГГц, рейтинг одного ядра 2040. У моего в турбо-режиме 4.3 ГГц, рейтинг одного ядра 1533. Да, не в 2 раза, а в 1.5 твой процессор быстрее. Рейтинг смотрел вот здесь: https://www.cpubenchmark.net.
УдалитьПро цепочки. Я сначала написал, потом подумал и понял, что в данной задаче они ни к чему. Даже если и образуется несколько небольших цепочек, большого влияния на производительность в среднем они не окажут.
Да, в случае двоичного поиска практически рандомный доступ к памяти получается. Я думаю, после третьей итерации поиска весь массив серий оказывается в кэш-памяти третьего уровня. Но этого все равно недостаточно, даже из кэша работает не шустро. Но все же основная проблема - это количество итераций. Ведь при поиске с хэш-таблицей мы тоже скорее всего в кэш не попадем, поэтому производится операция с памятью. Но одна чаще всего (при 40% заполнении). А в случае двоичного поиска (п.3 из твоего последнего сообщения) сначала операция в памяти по основному массиву, а затем 9 операций в массиве серий. Т. е. должно было бы быть в 10 раз медленнее по сравнению с самым быстрым вариантом из п. 1, а медленнее всего в 7 раз. Вот примерно настолько помогает кэш.
50/50 медленнее в случае хэш-таблиц, чем 100% промах скорее всего потому что вариант 50/50 случайно попадает на длинные цепочки, а 100% - нет. В других случаях может быть и наоборот.
Ну а в общем да, хэш-таблицы хороши, когда памяти с избытком, ключи короткие и не нужно в процессе работы ключи регулярно добавлять/удалять. Да, и сортировать по ключу не нужно еще.
> Да, не в 2 раза, а в 1.5 твой процессор быстрее.
УдалитьДа похоже, да это не важно. Ваши и мои результаты на "Массиве из массивов из short" очень близки, мне было интересно сравнить структуры данных, а не скоростью с Вами померяться :)
> а затем 9 операций в массиве серий... должно было бы быть в 10 раз медленнее..., а медленнее всего в 7 раз.
У меня ровно 1^20 маленьких массивов длинной от 69 до 155 (~ 2^7) так что "7 раз" это думаю с этим связано.
> Но все же основная проблема - это количество итераций.
Я понимаю что операций больше в разы... я хотел сказать что мало того что их физически больше, так они еще и данных из памяти загружают больше. То что именно память узкое место легко проверить:
1-й эксперимент: вызываем бинарный поиск 20 раз на запрос вместо 1-го, вот так по простому:
int binarySearch20(short[] a, short key) {
int s = 0;
for (int i = 0; i < 20; i++) {
s += binarySearch(a, key);
}
return s;
}
и получаем 1.7 MOPS что всего в 2.4 раза меньше, хотя операций аж в 20 раз больше!
2-й эксперимент: вместо проверки 10М уникальных номеров (все мои предыдущие эксперименты), проверяем всего 10 (кэш должен быть счастлив). Получаем прям бешеную скорость:
- хэш таблица (40%): 254 MOPS
- массив из массивов: 95 MOPS
- массив из массивов с "медленным" бинарным поиском из эксперимента выше: 6.6 MOPS, что в 14 раз медленнее, и это ожидаемо, так как теперь именно процессор, а не память узкое место и каждая инструкция замедляет работу.
П.С. А еще я попробовал bit-set на массиве long-ов, всего-то надо 1.2GB... Скорость 32 MOPS :)
> да это не важно. Ваши и мои результаты на "Массиве из массивов из short" очень близки, мне было интересно сравнить структуры данных
УдалитьМы везде приводим абсолютные числа, которые получены на разных машинах и в разных средах разработки. Поэтому неплохо бы знать абсолютную производительность, если сравнивать полученные нами результаты. Ну или сравнивать в рамках только одной машины. )
> У меня ровно 1^20 маленьких массивов длинной от 69 до 155 (~ 2^7) так что "7 раз" это думаю с этим связано.
Опечатка? 1^20 = 1. Не очень понял, в каком случае у тебя получается столько массивов.
Я вел рассуждение об основном массиве номеров длиной в 1 миллион элементов. Проверял пару дней назад, максимальная длина серий составляет 207. Средняя примерно 110-115. Ну да, тут и в самом деле получается примерно 7 операций в бинарном поиске.
Непонятно тогда, почему с поиском по большой хэш-таблице разница получается именно во столько раз. Кэш процессора не работает, кэширую ровно то значение, которое мы читаем, в данном случае short. Кэш читает сразу много данных из памяти за раз. Не знаю, какое количество считается на современных процессорах, но точно не меньше 16 байт. Поэтому пара последних операций в бинарном поиске с очень большой вероятностью попадает в кэш, а скорее всего и три последних операции. Поэтому теоретически отношение должно быть меньше. Странно, что это не так.
> То что именно память узкое место легко проверить
В общем-то я и не спорил, что память в случае двоичного поиска является узким местом. Мне любопытно было оценить влияние кэша на производительность.
> А еще я попробовал bit-set на массиве long-ов, всего-то надо 1.2GB... Скорость 32 MOPS :)
Кстати, я тоже. ) Но ты раньше написал. Памяти у меня заняло столько же, а вот производительность в два раза меньше. Затрудняюсь сказать, в чем тут дело. Ну, в полтора раза отстает мой процессор, а вот куда делось еще полраза, непонятно. )
Как на Java реализуется бит-сет? Может, разница в реализации? У меня это просто двухмерный массив.
Ну и похоже, по совокупности параметров этот вариант наилучший для данной задачи. И производительность супер, и в памяти очень компактен.
> Опечатка? 1^20
УдалитьЯ просто неудачный символ выбрал для возведения в степень :)
> с поиском по большой хэш-таблице
На самом деле я тестирую массив из 3-х хэш таблиц (так они памяти меньше кушают), а скорость +/- одинакова с одной большой хэш таблицей.
> но точно не меньше 16 байт.
Все так, cache line size - 64 байтов на моем CPU
> Поэтому пара последних операций в бинарном поиске с очень большой вероятностью попадает в кэш...
Согласен, 2-3 обращения к памяти должно по любому хватить, так как за раз загружается 64 / 2 = 32 числа. Плюс сколько-то уходит на обращение в 1М массиве, так как он занимает у меня 8M (64-bit система), а L3 cache всего 6 Мб.
По замеру времени разница в 5 раз (20 vs 4.2 MOPS), по операциям выходит где-то в 3-4 раза, плюс надо учесть что чем больше загружаем, тем меньше вероятность что что-то будет в кэше. Как мне кажется все более менее сходится...
> У меня это просто двухмерный массив.
У меня одномерный массив из long (я сразу при чтении из файла превращаю номер + серию в long и забываю о них). Проверка на наличие номера стандартная:
(bitmap[(int)(v / 64)] & (1L << (v % 64))) != 0 (оптимизатор заменит / и % на сдвиг и битовые операции)
А у Вас случайно не массив из указателей на массивы? В таком случае как раз в 2 раза больше обращений к памяти.
1^20 :))) Да конечно 2^20, только сейчас перечитал что накалякал утром и понял о чем Вы...
УдалитьНет, у меня двухмерный массив. По одной размерности номер паспорта, по другой - битовый массив серий.
УдалитьПроверка выглядит вот так: (data[Number][Series div 32] and (1 shl (Series mod 32))) <> 0
Идея одномерного мне чего-то в голову не пришла. Попробовал. При нахождении паспорта производительность оказалась такой же, как в случае двухмерного массива. А вот при отсутствии паспорта просела в два раза. Хочу заметить, что я все в 32-разрядной системе делаю. Непонятно, откуда такая двукратная разница при выполнении одной и той же операции на разных исходных данных. Надо будет покопаться, а то может быть где накосячил на тесте.
Даже не знаю...
Удалить1. Может быть Delphi div & mod на степень 2-ки не оптимизирует?
Ради эксперимента обманул Java оптимизатор не дав ему заоптимизировать / и %. Скорость сразу просела аккурат в 2 раза до 16 MOPS :)
Попробуйте что-нибудь вроде: (data[Number][Series shr 5] and (1 shl (Series and 31))) <> 0
2. Может быть сама инфраструктура тестирования вносит замедление?
У меня для проверки этого есть фейковый алгоритм (num % 2 == 0) результат выдает какой попало :), но зато супер-быстро. Его скорость 450-500 MOPS на моем процессоре и коде что я нагородил для тестирования.
Нет, с оптимизацией при умножении и делении на степень двойки у Delphi все нормально )
УдалитьТут в последнем моем эксперименте скорее демонстрация однобокости синтетических тестов, ну и заодно влияние кэша на производительность процессора.
1. При переходе к одномерному массиву такого размера на 32-битных системах сразу сталкиваемся с необходимостью выполняться операции на 64-разрядными числами, что сразу должно приводить к некоторому снижению производительности, однако в тесте при попадании паспорта в базу такого почему-то не произошло.
2. Первоначально я решил, вообще не думая, формировать полный идентификатор паспорта так: Series*1000000+Number. Собственно, почему нет?
3. Так как performance counter в Windows достаточно точный, то я ограничился при тестировании 100000 прогонов номеров для каждого случая (попадание/непопадание).
4. Для попадания я просто брал первые 100000 записей из загруженного файла. На первый взгляд данные там выглядели достаточно хаотично. Но, как выяснилось в дальнейшем, только на первый взгляд.
5. Для непопадания я брал следующие 100000 записей из того же файла, используя номер паспорта и случайно выбирая такую серию, которой не было в предоставленных данных.
6. В результате анализа у меня получилось, что данные из п. 4, только выглядели хаотично, на самом деле при хранении собирались в достаточно компактную область, которая большей частью полностью попадала в кэш третьего уровня процессора при тестировнии. Что и дало такую же производительность, как и вариант с двухмерным массивом, несмотря на более сложные операции с длинной арифметикой. А вот в случае данных из п. 5 они оказались при поиске хаотично разбросаны по памяти и практически никогда в кэш не попадали.
7. При замене расчета идентификатора паспорта на Series+Number*10000 получил примерно ожидаемую цифру в 9 MOPS для обоих случаев.
Провел еще один эксперимент. Вместо замены метода расчета идентификатора паспорта, поменял генерацию данных для тестирования непопадания (п. 5 из предыдущего комментария). В этот раз брал существующую серию и генерировал случайный номер, которого нет в данных. Результат удивительный: 18 MOPS при попадании и 12 MOPS при промахе.
УдалитьНу и напоследок повторение предыдущего эксперимента на полностью случайных данных. При промахе 8 MOPS.
Удалить> Результат удивительный
УдалитьУ меня вроде есть объяснение такому ускорению. Я посмотрел на 1-е 100К паспортов и распределение по сериям совсем не равномерное: есть серии с 1 паспортом, а есть и с 5028. На 50 наиболее частных серий приходится ~60К паспортов, так как 2-й индекс в массиве - это номер, то вероятность что он уже в кэше сильно повышается.
Раз Вы раскрыли тайну генерации запросов :), то вот как я делаю:
Тестирую на 10М запросов. Ровно половина - случайно выбранных из всего файла, вторая половина - случайно сгенерированные в диапазоне (мин. номер, макс. номер) и проверяю что их нет в файле. Все запросы случайно перемешиваю. Вроде Ваш последний "на полностью случайных данных" эксперимент близок к тому что я делаю.
Сейчас попробовал запустить на 100К запросах и результат гуляет от 8 до 40 MOPS. Java - она совсем не про реалтайм, то сборщик мусора замедлит, то компилятор решит код перестроить...
Также глянул на генерируемый ассемблер, он почти один в один с кодом что я писал выше (одна загрузка из памяти и несколько простых операций над регистрами), правда Java заинлайнила все по максимуму и в итоге тестируемый код совсем без вызовов методов :)
Попробовал отключить инлайнинг скорость просела до 20-25 MOPS.
> У меня вроде есть объяснение такому ускорению. Я посмотрел на 1-е 100К паспортов и распределение по сериям совсем не равномерное: есть серии с 1 паспортом, а есть и с 5028. На 50 наиболее частных серий приходится ~60К паспортов, так как 2-й индекс в массиве - это номер, то вероятность что он уже в кэше сильно повышается.
УдалитьИзвините, я только сейчас понял про что Вы говорили в пункте 6 как раз об этом. Там на одну серию приходится целых 12К номеров, а на 10 серий 50К...
Кстати по поводу 32 vs 64-bit. В Java все объекты в куче выровнены по 8 байт как результат 32 битных указателей достаточно чтобы адресовать 32 ГБайта, чем Java и пользуется по дефолту. Правда на наших тестах это не пригодилось...
ОтветитьУдалить