Ужасы программирования
lexa - 18/Ноя/2008 17:11
Сортирую записи по 8-байтным ключам в byte order (не в numeric). Записей много (сотни миллионов). Оказывается, сравнивать ключи надо так:
unsigned char *p1 = (unsigned char *)d1;
unsigned char *p2 = (unsigned char *)d2;
#define CMP(i) if(p1[i]!=p2[i]) return p1[i]-p2[i]
CMP(0); CMP(1); CMP(2); CMP(3); CMP(4);CMP(5); CMP(6); CMP(7);
return 0;
#undef CMP
А банальный memcmp/__builtin_memcmp оказывается в 2.2 раза медленнее на общее время исполнения qsort(), во сколько раз различается сравнение - боюсь думать.
С numeric-сортировкой тоже весело, естественно наступил на (value2-value1) - это быстро и неверно (а верный вариант - получился не быстрее побайтового).
Comments
qsort из libc ? А если std::sort и чтобы operator < у сра
qsort из libc ? А если std::sort и чтобы operator < у сравниваемых элементов инлайнился ?
Byte order, а не numeric. С числовым порядком все несколько
Byte order, а не numeric. С числовым порядком все несколько проще.
А кстати, как в std:vector сделать read() ?
А кстати, как в std:vector сделать read() ?
Гы, а никак по-нормальному :)
Гы, а никак по-нормальному :)
В-общем, в рафинированом случае с 18млн ключей без данных -
В-общем, в рафинированом случае с 18млн ключей без данных - std::sort быстрее на numeric раза в полтора. Но копирование - довольно существенная часть и в реальном случае, когда на 20 байт данных 8 байт ключа - примерно один хрен.
Да, и конверсия просто буфера в вектор - это вдвое по памяти
Да, и конверсия просто буфера в вектор - это вдвое по памяти, а читать кусочками - тоже никакой радости.
А в чём вопрос для такого специфического момента ручками зак
А в чём вопрос для такого специфического момента ручками закодировать qsort?
Жизнь коротка. Вот эта хрень, которая выше - это ускорение д
Жизнь коротка. Вот эта хрень, которая выше - это ускорение двое за 5 минут, размялся и приятно.
А реальный варез все равно тормозит при записи на диск (собственно, его то я и пытаюсь ускорить на порядок-другой-третий)
А в какой операционке ? Во FreeBSD memcmp выглядит так: do {
А в какой операционке ? Во FreeBSD memcmp выглядит так:
do {
if (*p1++ != *p2++) return (*--p1 - *--p2);
} while(--n != 0);
Что есть обощенный вариант вашего решения.
FreeBSD 6.4 x64 Но вы привели библиотечный memcmp, а естест
FreeBSD 6.4 x64
Но вы привели библиотечный memcmp, а естественно gcc будет ставить builtin. В исходних ассемблерный не смотрел, но говорят там repe cmpsb (сужу по выдаче гугла)
Ну а дальше оказывается, например, что loop unrolling при нонешней глубине конвейера - полезен.
Пардон, писал не проверив. Библиотечный memcmp в FreeBSD леж
Пардон, писал не проверив.
Библиотечный memcmp в FreeBSD лежит в {amd64,i386}/string/memcmp.S а вовсе не в string/memcmp.c (последний вариант - для совместимости и в реальной жизни никуда не попадет).
И скорее всего на длинных сравнениях он выиграет, как только цена call станет невидной. Но у меня то 8 байт.
1. стандартная сортировка C++ (с функтором) может быть быстр
1. стандартная сортировка C++ (с функтором) может быть быстрее qsort() из C.
2. а если попробовать что-то вроде (для 3-х) return (a1==b1)?((a2==b2)?((a3==b3)?0:a3-b3):a2-b2):a1-b1 ?
Да, в рафинированном (только ключи) случае std::sort(vector.
Да, в рафинированном (только ключи) случае std::sort(vector...) быстрее.
Если ключи с данными (данных - больше), то время на копирование съедает выигрыш.
ну тогда можно отсортировать сначала индекс, а потом по нему
ну тогда можно отсортировать сначала индекс, а потом по нему реальный массив. ещё вместо std::vector можно использовать просто динамические массивы (вроде в boost есть).
хотя, все эти "извращения" с оптимизацией не нужны, если сортировка не hot spot.
У меня оно во входном файле лежит вместе (записями фиксирова
У меня оно во входном файле лежит вместе (записями фиксированной длины).
Ну и конечно не hot spot - тестовый файл порождается 3 минуты, сортируется 3 секунды, а вот в индексированный вид (BTree) сейчас пишется 3 часа (ну а моя задача писать в Btree еще за несколько минут, хотя не уверен в возможности, экспериментирую)
В смысле, я к тому что это все 5-минутное развлечение, давше
В смысле, я к тому что это все 5-минутное развлечение, давшее неожиданный результат (начал я с того, что переполнил int в numeric sort и у меня количество уникальных ключей не расходилось в маленьких тестах, но расходилось на миллионах - тоже, обычные такие грабли, но не каждый день бываают)
прости за дельтанский вопрос а не будет ли быстрее сравниват
прости за дельтанский вопрос а не будет ли быстрее сравнивать два лонга (или четыре инта)?
тоесть за сколько тактов они сравниваются на ассемблер современных процов
Потому что это byte order, а не численный. Байты в лонгах-и
Потому что это byte order, а не численный.
Байты в лонгах-интах идут не подряд и порядок (результат) сортировки для двух способов сравнения получается разный.
Может я неправильно понял задачу: а <a href="http://en.wikip
Может я неправильно понял задачу: а radix sort тут не поможет?
Да может и поможет, но где реализация в stdlib?
Да может и поможет, но где реализация в stdlib?
<b>простите,</b><br/> не догоняю, почему вместо if(a!=b) ret
простите,
не догоняю, почему вместо if(a!=b) return a-b не писать сразу a-b?
<b>Re: простите,</b><br/> А если эти байты равны?
Re: простите,
А если эти байты равны?
<b>Re: простите,</b><br/> будет 0. не вижу пока проблемы.
Re: простите,
будет 0. не вижу пока проблемы.
<b>Re: простите,</b><br/> Ну и как перейти к следующему байт
Re: простите,
Ну и как перейти к следующему байту без if, если мы побайтно сравниваем ?
Но Шабановский вариант мне конечно больше нравится, сейчас я его изучу.
вообще-то всё зависит от задачи. Грубо можно так, наверное u
вообще-то всё зависит от задачи. Грубо можно так, наверное
unsigned int64 *p1 = (unsigned int64 *)d1;
unsigned int64 *p2 = (unsigned int64 *)d2;
return p1[0]-p2[0];
Еще раз: мне нужно отсортировать не по числовому значению,
Еще раз:
мне нужно отсортировать не по числовому значению, а как "8-байтовые строки".
На x86 (в широком понимании, включая сюда и x64) - это разные сортировки.
сравнение как return (a > b) - (b > a) генерирует 2
сравнение как
return (a > b) - (b > a)
генерирует 2 команды cmove и ни одного jmp. cmove тоже, конечно, не сахар, спекулятивно исполнять процессору трудно, но лучше чем jmp.
Ещё помогает htonl каакой-нить, он инлайнится в одну команду bswap на 64 битах.
Короче, я за
a1 = betoh64(a);
b1 = betoh64(b);
return (a1 > b1) - (a1 < b1);
а как из них цепочку составить?
а как из них цепочку составить?
а зачем цепочку? Сравниваем 8-байтные числа и всё.
а зачем цепочку? Сравниваем 8-байтные числа и всё.
см. выше, это не правильно.
см. выше, это не правильно.
куда выше? сравниваем 2 восьмибайтных ключа. Где здесь цепо
куда выше?
сравниваем 2 восьмибайтных ключа. Где здесь цепочка?
ок. вы правы. просто это не общее решение, допустим, если на
ок. вы правы.
просто это не общее решение, допустим, если надо поменять лексикографический порядок, то нужно будет переставлять биты.
А там не надо цепочки, мне же нужно просто сравнить два 8-ба
А там не надо цепочки, мне же нужно просто сравнить два 8-байтных ключа в byte order.
А эти betoh64 - оно где так называется? А то на FreeBSD оно
А эти betoh64 - оно где так называется?
А то на FreeBSD оно be64toh. и естественно bswap64 на интеле и ничего на "мотороле" (в смысле - Сане)
это я перепутал, лень было грепать /usr/include/machine
это я перепутал, лень было грепать /usr/include/machine
Тьфу, пропасть, как же скормить "меньше" комментар
Тьфу, пропасть, как же скормить "меньше" комментариям? Третья попытка
А я проверил, мне не жалко.
18млн ключей (никаких других данных нет)
2300 msec (округляю до 100ms) - мой вариант
3900 msec - твой вариант.
Возможное объяснение в том, что пространство ключей большое, распределены они случайно (ибо это хэши fnv64), поэтому первое же сравнение дает разницу в 255 из 256 случаев.
При этом, сама схема с cmove - хорошая, для numeric sort:
(a>b)-(a < b) - 3500 msec
а мой тупой вариант
(a>b)?1:((a < b)?-1:0)) - 3800 msec.
Ну и std::sort для numeric - 2300 ms - включая инициализацию std::vector,
а если ее не считать - то 1800ms - инлайнинг и алгоритм считается получше чем qsort.
ммм... мы на других данных получили противоположный результа
ммм... мы на других данных получили противоположный результат, но у нас было 2 сравнения (16 байт) и отсортированные ключи, поэтому различия были обычно не в первом байте.
А ведь не выравнено? Возможно, два be32toh быстрее будут, т.к. при чтении 8 байт процессор может за двумя строками полезть.
Если не в лом, кинь чтоли дизассемблированный код обоих вариантов на гмыло vlad.shabanov. Самому интересно стало.
Я по дизасемблеру отдельный пост напишу, только детей в школ
Я по дизасемблеру отдельный пост напишу, только детей в школу сдам...
С чего ему быть невыровненым, если malloc возвращает aligned
С чего ему быть невыровненым, если malloc возвращает aligned на 16 (а в данном тесте вообще адрес буфера кончается на d00000)?
Потом я туда read прямо из файла, где 8-байтовые штуки лежат вплотную. Получается, всегда выровнено на 8 байт, ровнее не бывает.
(вот кстати реальные данные, а не просто ключи - 20-байтные + 8 байт ключа, интересно, будет ли быстрее, если подровнять).
А пока я не полез профайлить, почему ты считаешь что для *со
А пока я не полез профайлить, почему ты считаешь что для *современного* интела cmove лучше чем jmp (не indirect, конечно)? Мне представляется что это одно и то же, и оба спекулятивно исполняются.
Чисто интуитивно, при cmove у процессора только непонятки со
Чисто интуитивно, при cmove у процессора только непонятки со значением регистра, а при jmp -- с тканью судьбы.
Если сгенерировался хороший код исходного варианта (mov; cmp; jmp; mov; cmp; jmp; ...), то получается 24 команды, в которых mov и cmp prefetch-ится, но толком не параллелятся, будем считать, что в среднем исполняется 12 штук. А если сгенерировался хороший код (mov; bswap; mov; bswap; cmp; cmovl; cmova; sub), то 5 команд, т.к. 1 и 3, 2 и 4, 6 и 7 могут параллелиться.
Когда первые байты сильно различаются, то на тысячной итерации всё предсказано и получаются 3 команды, что, конечно, бъёт наповал.
Только мне кажется, что оба варианта говённо скомпилировались и дело в волшебных пузырьках.
Ждём постинга с дизассемблированным кодом.
Ну вот для первого варианта так и получилось примерно (mov,m
Ну вот для первого варианта так и получилось примерно (mov,mov,cmp,jne).
А вот cmove пытаюсь получить по твоему варианту, пока никак. Уже третий вид gcc компилирую (попробовал 3.4 и 4.1, сейчас компилирую 4.3). Все что получалось пока - cmpq/seta/cmpq/setb/mov/sub)
Если и 4.3 не сделает твоего варианта - буду подробных показаний спрашивать.
а bswap правильно заинлайнился?
а bswap правильно заинлайнился?
Да, bswap - правильно. Но даже с gcc43 -O3 -mtune=core2 -ma
Да, bswap - правильно.
Но даже с gcc43 -O3 -mtune=core2 -march=core2 получается (все в регистрах)
cmpq
seta
cmpq
setb
movzbl
subl
ret
Может я каких ключей недодал? Хотя код вполне разумный на самом деле....
да ладно, это как в анекдоте про преферансистов. cmova/cmov
да ладно, это как в анекдоте про преферансистов.
cmova/cmovb вряд ли отличается по времени от seta/setb а как заставить компилятор не делать лишнего сравнения я не знаю. Даже если заставить, всё равно будет не 3 команды.
Ну, мне скорее интересно, откуда таки взялось cmov. Ибо для
Ну, мне скорее интересно, откуда таки взялось cmov. Ибо для всех 64-битных архитектур gcc43 делает совершенно одинаковый код с -O2/O3 (для меньшей оптимизации начинаются всякие фокусы, -O0 - просто чудо природы).
А так - конечно пофигу.
cmov я руками писал когда-то, ещё в прошлой жизни. Распаковы
cmov я руками писал когда-то, ещё в прошлой жизни. Распаковывал координаты в Рамблеровском индексе.
Присвоить звание "слесарь-компилятор шестого разряда&qu
Присвоить звание "слесарь-компилятор шестого разряда"
И, кстати, интересная мысля - посчитать частоты исполнения к
И, кстати, интересная мысля - посчитать частоты исполнения кусочков первого варианта. Тоже сделаю....