Ответ 1
На основании информации, заданной в вопросе, можно только догадываться и называть некоторые идеи.
Вы правильно оценили? Имейте в виду, чтобы получить надежные результаты работы, нужно (по крайней мере)
- Запустить выпуск сборок без прикрепленного отладчика
- Повторите тест достаточно часто и усредните результаты
- Сделайте тест честным, т.е. дать всем испытуемым ту же "конфигурацию ressource"
Чтобы убедиться, что источник (предполагаемого) снижения производительности действительно подключен к встроенной функции, можно исследовать сгенерированный код IL. Или еще лучше: машинные инструкции, сгенерированные компилятором JIT.
Для ILNumerics мы внедрили пользовательскую быструю сортировку и сделали много показателей производительности. Окончательный алгоритм в несколько раз быстрее, чем версия CLR. Ручная вставка - это только одно усовершенствование, которое необходимо для повышения производительности. Другие:
- Не использовать рекурсию
- Использование небезопасного сравнения/замены
- Использование сортировки вставки для небольших разделов
- Оптимизация для ограниченного размера временного массива замены стека
Очень часто источник странных результатов производительности найден в способе, память (неверно) используется алгоритмом. Другой может заключаться в различном потоке команд, который в конечном итоге более или менее преуспевает в оптимизации любого компилятора/процессора. Общая производительность исполнения - очень сложный зверь, трудно угадать детерминистически и, следовательно, профилировщик - ваш лучший друг!
@Edit:. Посмотрев на основную тестовую процедуру, вы, в основном, измеряете пропускную способность памяти вашего процессора/основной памяти. Массив int [] длиной 5 * 10e6 имеет размер около 19 МБ. Это, скорее всего, вне диапазона ваших кешей. Таким образом, процессор будет ждать памяти из-за обязательных кэш-пропусков больше всего времени. Это мешает влиянию любой переформулировки кода, которую трудно угадать. Я предлагаю попытаться измерить этот способ вместо этого: (псевдокод)
- генерировать тестовые данные
- выделить массив для копий
-
повторить число глобальных повторений (скажем, 10)
- внутренние повторения для Array.Sort(например, 1000)
- скопировать (несортированные) тестовые данные
- Сортируйте копию с помощью Array.Sort
- добавить время
-
среднее время для Array.Sort
-
внутренние повторения для Quicksort.Sort(например, 1000)
- скопировать (несортированные) тестовые данные
- сортируйте копию с помощью команды Quicksort.Sort
- добавить время
-
среднее время для Quicksort.Sort
-
внутренние повторения для Quicksort.Sort2 (например, 1000)
- скопировать (несортированные) тестовые данные
- Сортируйте копию по Quicksort.Sort2
- добавить время
- среднее время для Quicksort.Sort2
- внутренние повторения для Array.Sort(например, 1000)
Цель состоит в том, чтобы использовать quicksort только данные из кешей. Поэтому не заново создавайте копии из новой памяти, а имеете только два глобальных экземпляра: оригинал и копию для сортировки. Оба должны одновременно входить в кеш (последнего уровня)! С некоторым запасом прочности (для других процессов в системе) следует учесть только половину доступного размера кэша последнего уровня для обоих массивов. В зависимости от вашего истинного размера кеша тестовая длина 250 000 кажется более разумной.
@Edit2: Я запустил ваш код, получил те же результаты и просмотрел (оптимизированные) машинные инструкции в отладчике VS. Здесь идет соответствующая часть из обеих версий:
Not inlined:
69: do { pIndex--; } while (arr[pIndex] > pivot);
00000017 dec ebx
00000018 cmp ebx,esi
0000001a jae 00000053
0000001c cmp dword ptr [ecx+ebx*4+8],edi
00000020 jg 00000017
70: do { leftPoint++; } while (arr[leftPoint] < pivot);
00000022 inc edx
00000023 cmp edx,esi
00000025 jae 00000053
00000027 cmp dword ptr [ecx+edx*4+8],edi
0000002b jl 00000022
Inlined:
97: do { pIndex--; } while (arr[pIndex] > pivot);
00000038 dec dword ptr [ebp-14h]
0000003b mov eax,dword ptr [ebp-14h]
0000003e cmp eax,edi
00000040 jae 00000097
00000042 cmp dword ptr [esi+eax*4+8],ebx
00000046 jg 00000038
98: do { leftPoint++; } while (arr[leftPoint] < pivot);
00000048 inc ecx
00000049 cmp ecx,edi
0000004b jae 00000097
0000004d cmp dword ptr [esi+ecx*4+8],ebx
00000051 jl 00000048
Как видно, версия "Не встроенная" лучше использует регистры для уменьшения индексов работы (строка 69/97). Очевидно, что JIT решил не нажимать и не вставлять соответствующий регистр в стек, поскольку другой код в той же функции использует один и тот же регистр. Поскольку это горячий цикл (и CLR не распознает это), страдает общая скорость выполнения. Таким образом, в этом конкретном случае ручная установка функции разделения не выгодна.
Однако, как вы знаете, нет гарантии, что другие версии CLR сделают то же самое. Различия могут даже возникнуть для 64-битного.