Pull to refresh

Comments 36

UFO landed and left these words here
На K10 rdtsc выполняется за 67-71, на Core 2 — 30-32. Вообще у Агнера некоторые цифры из этого поста есть и гораздо больше. По Агнеру, на K10 rdtsc занимает 67, на Core 2 — 32 такта.
Я сам занимался данной проблемой некоторое время назад и хотелось бы поделиться своими замечаниями.

Процессор очень хитрое устройство, поэтому если писать что то такое:
ull t1 = rdtsc();
for (int i = 0; i < inner_len; i += VEC_LEN) {
записать синус angles[i] в sines[i]
}
ull t = rdtsc() - t1;

то может получиться так, что процессор поменяет инструкции местами(так как они не зависят), поэтому rdtcs() может вызваться не там где нужно и результат будет не верным.

Мое решение следующие(GCC only). Я не стал приводить весь код, только основное:

inline void start()
{
	asm volatile (  "cpuid\n\t"
					"rdtsc\n\t"
					"mov %%edx, %0\n\t"
					"mov %%eax, %1\n\t" : "=r"(time_edx), "=r"(time_eax) ::
					"%rax", "%rbx", "%rcx", "%rdx");
}

inline void stop ()
{
	asm volatile (  "rdtscp\n\t"
					"mov %%edx, %0\n\t"
					"mov %%eax, %1\n\t"
					"cpuid\n\t" : "=r"(time_edx1), "=r"(time_eax1) ::
					"%rax", "%rbx", "%rcx", "%rdx");

	time_last =
				(static_cast<unsigned long long>(time_edx1) << 32 | static_cast<unsigned long long>(time_eax1)) -
				(static_cast<unsigned long long>(time_edx)  << 32 | static_cast<unsigned long long>(time_eax));
}


Рассмотрим подробнее, как устроен данный код.

Вначале необходимо вызвать инструкцию cpuid для того, чтобы процессор не менял порядок исполнения инструкций. Затем, вызывая инструкцию rdtsc, происходит запись количества тактов процессора в регистры edx и eax.
Инструкция rdtscp читает значение количества тактов процессора и сохраняет их в регистры edx и eax, гарантируя при этом, что весь код, который находится до этой инструкции, будет выполнен. После данной инструкции, также стоит вызвать инструкцию cpuid, что бы предотвратить внеочередное исполнение инструкций.

Следует заметить, что на «замеряемое» время это ни как не повлияет, т.к. инструкция cpuid следует за инструкцией rdtscp(которая появилась только в процессорах начиная с Intel Core i7)

Подробности можно посмотреть тут: Gabriele Paoloni. How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures, September 2010
> то может получиться так, что процессор поменяет инструкции местами(так как они не зависят), поэтому rdtcs() может вызваться не там где нужно и результат будет не верным.

А можно ли для этого применять volatile переменные и memorybarier (читал здесь: ru.wikipedia.org/wiki/GCC_Inline_Assembly)? И если нет, то почему?
asm volatile — это просьба к компилятору, чтобы он ничего не трогал, но к процессору это никакого отношения не имеет.
Синтетика такая синтетика. Какой смысл в замере времени выполнения нескольких инструкций в идеальных условиях?
UFO landed and left these words here
По идее профайлер должен позволить лучше понять микроархитектурные феномены, у него есть доступ к апаратным счетчикам.

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

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

А вот какой смысл гонять бенчмарк на 2-3 инструкции, которые в реальности будут исполняться в потоке других инструкций совершенно по-другому, я решительно не понимаю.
UFO landed and left these words here
А что делать, если наш код может запускаться на десятках разных процессорах, отличающихся в разы размером кеша, длиной ковеера, числом ALU, наличием или отсутствием внеочередного исполнения инструкций и так далее и так далее?
UFO landed and left these words here
И шипить отдельный вариант кода для каждого процессора? Не ожидал что все настолько серьезно.
UFO landed and left these words here
Десятки разных процессоров — где ж вы их возьмете?
Для мейнстрима (х86) оптимизация для Intel даст прирост на AMD с вероятностью 95%.
Core i7 и Intel Atom отличаются достаточно сильно.

Утверждается, что для мейнстрима (x86) есть куча вариантов, отличающихся в разы размером кеша, длиной ковеера, числом ALU, наличием или отсутствием внеочередного исполнения инструкций и так далее и так далее.

Я конечно не специалист в этом вопросе, но кажется мне, что оптимизировав код на пределе возможностей конкретного камня, мы получим нечто, работающее далеко не так хорошо на других камнях. В лучшем случае, скорость будет как у любого другого кода, который специально не оптимизировался под микроархитектурные особенности. В худшем — сильно просядем к примеру из-за того что данные перестали хорошо укладываться в кеш.
Ну так десяток где то?
Для атома, если очень приспичит — напишете свою версию.
Хотя даже не так — делается «общая версия» — по возможности тупая, с минимальной базой х86 (например П3). Дальше детектор спецификации процессора и переключение ветки кода на оптимизированную если архитектурные ограничения позволяют ее выполнить.

Насчет размеров кешей и прочего — архитектурно-оптимизированные библиотеки частенько поставляются сорцами (см. Atlas, FFTW) и когда вы ее компилируете на целевом процессоре все автоматически подгоняется. А те библиотеки как MKL работают по описанному выше алгоритму — ветки кода и детектор процессора.

Если разницу в скорости двух различных реализаций нельзя увидеть таймерами, то точно также её нельзя будет увидеть в production системе.
А разве нужна интуиция при проведении оптимизации производительности кода?
Как по мне, то нужно полагаться на показания профайлера, а не на интуицию.
UFO landed and left these words here
Звучит красиво, а конкретные примеры есть?

По мне, если уж очень хочется оптимизировать на таком низком уровне, проще использовать генетические алгоритмы для определения оптимального порядка инструкций, чем вручную возиться.
UFO landed and left these words here
Процессорное время всяко дешевле времени людей, как минимум нужно комбинировать оба подхода.

Такие вещи есть смысл делать только, когда все алгоритмические оптимизации исчерпаны. Значит у нас есть более-менее конечный вариант кода сгенерированный оптимизирующим компилятором. Простая перестановка инструкций это конечно слегка примитивно, имеет смысл добавить «эквивалентные преобразования кода» (например haddp <-> shufp + addp и т.д.).

Понятно что сразу сесть и написать их все будет трудновато. Но если список расширять каждый раз когда «человек оказался умнее машины», то очень скоро можно будет в 99% случаев просто закинуть программу на кластер и идти пить чай)))
UFO landed and left these words here
Если тонкая оптимизация выявила потребность в изменении семантики, значит кто-то сильно накосячил раньше на этапе алгоритмических оптимизаций и профилирования))

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

Но это дело вкуса, по мне так выигрыш не стоит затраченных усилий. Даже программы которые пускают на Tier 1 кластеры (а это слоты на миллионы CPU часов) никто так не вылизывает.
Я понимаю, что немного :)
Применимость: сравнение однопоточного потенциала процессоров, просто интерес.
Зачем городить огород с RDTSC когда есть perf/oprofile и наносекундные таймеры?
В первом примере «поверхностный подход» — это именно использование наносекундных таймеров.

Повторюсь, мой метод к профилированию отношения не имеет.

Вообще, в посте rdtsc и такты можно заменить на какой-нибудь performance counter, например «uops scheduled», без особой потери смысла. Многие советы лежат в другой плоскости.
> Поэтому часть цикла между скобками либо должна быть написана на ассемблере, либо на Си, но вы должны четко понимать, чего добиваетесь от компилятора.

Раскройте пожалуйста смысл.
Если пишете на Си, проверьте с помощью ключа -S генерируемый ассемблерный код. Чтобы понимать, что вообще измеряется. Иногда компилятор может наломать дров с разворачиванием циклов и т. д.
О, теперь дошло!

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

Возможно ошибаюсь, но наверное имелось в виду не начальное и конечное RDTSC, а разница между начальным и конечным RDTSC?
Время выполнения rdtsc не всегда зафиксировано четко. Если 1-я rdtsc займет 29 тактов, а 2-я 31, то время непосредственно измеряемого кода будет выведено на 2(?) такта больше, чем если 1-я rdtsc займет 31, 2-я — 29 тактов.
> Время выполнения rdtsc не всегда зафиксировано четко.

Мда… Сколько же тут подводных камней…
Only those users with full accounts are able to leave comments. Log in, please.