Я использую Windows 10 (1607) на процессоре Intel Xeon E3-1231v3 (Haswell, 4 физических ядра, 8 логических ядер).
Когда я впервые установил Windows 7 на эту машину, я заметил, что четыре из восьми логических ядер были припаркованы, пока приложению не потребовалось более 4 потоков. С помощью монитора ресурсов Windows можно проверить, припаркованы ли ядра или нет (пример). Насколько я понимаю, это важная техника для поддержания баланса потоков между физическими ядрами, как объясняется на веб-сайте Microsoft: « Алгоритм и инфраструктура парковки ядра также используются для балансировки производительности процессора между логическими процессорами в клиентских системах Windows 7 с помощью процессоры с технологией Intel Hyper-Threading ».
Однако после обновления до Windows 10 я заметил, что нет базовой парковки. Все логические ядра активны все время, и когда вы запускаете приложение, используя менее четырех потоков, вы можете увидеть, как планировщик равномерно распределяет их по всем логическим ядрам процессора. Сотрудники Microsoft подтвердили, что базовая парковка отключена в Windows 10.
Но мне интересно, почему? Какова была причина этого? Есть ли замена и если да, то как это выглядит? Разработала ли Microsoft новую стратегию планировщика, которая сделала парковку ядра устаревшей?
Приложение:
Вот пример того, как базовая парковка, представленная в Windows 7, может повысить производительность (по сравнению с Vista, у которой пока нет базовой парковки). Вы можете видеть, что в Vista HT (Hyper Threading) снижает производительность, а в Windows 7 - нет:
(источник)
Я попытался включить Core Parking, как упоминалось здесь, но я заметил, что алгоритм Core Parking больше не поддерживает технологию Hyper Threading. Он припарковал ядра 4,5,6,7, в то время как он должен был припарковать ядра 1,3,5,7, чтобы избежать назначения потоков одному и тому же физическому ядру. Windows перечисляет ядра таким образом, что два последовательных индекса принадлежат одному физическому ядру. Очень странно. Кажется, Microsoft все испортила в корне. И никто не заметил ...
Кроме того, я сделал несколько тестов процессора, используя ровно 4 потока.
Сродство к процессору установлено для всех ядер (Windows defualt):
Среднее время работы: 17.094498, стандартное отклонение: 2.472625
Привязка к ЦП устанавливается для каждого другого ядра (чтобы оно работало на разных физических ядрах, наилучшее планирование):
Среднее время работы: 15.014045, стандартное отклонение: 1.302473
Привязка ЦП установлена на худшее из возможных расписаний (четыре логических ядра на двух физических ядрах):
Среднее время работы: 20.811493, стандартное отклонение: 1.405621
Так что есть разница в производительности. И вы можете видеть, что планирование по умолчанию в Windows ранжируется между лучшим и наихудшим возможным планированием, как мы ожидаем, что это произойдет с планировщиком, не поддерживающим гиперпоточность. Однако, как указано в комментариях, могут быть и другие причины, такие как меньшее количество переключений контекста, логический вывод приложений и т.д. Таким образом, мы до сих пор не имеем однозначного ответа.
Исходный код для моего теста:
#include <stdlib.h>
#include <Windows.h>
#include <math.h>
double runBenchmark(int num_cores) {
int size = 1000;
double** source = new double*[size];
for (int x = 0; x < size; x++) {
source[x] = new double[size];
}
double** target = new double*[size * 2];
for (int x = 0; x < size * 2; x++) {
target[x] = new double[size * 2];
}
#pragma omp parallel for num_threads(num_cores)
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
source[y][x] = rand();
}
}
#pragma omp parallel for num_threads(num_cores)
for (int x = 0; x < size-1; x++) {
for (int y = 0; y < size-1; y++) {
target[x * 2][y * 2] = 0.25 * (source[x][y] + source[x + 1][y] + source[x][y + 1] + source[x + 1][y + 1]);
}
}
double result = target[rand() % size][rand() % size];
for (int x = 0; x < size * 2; x++) delete[] target[x];
for (int x = 0; x < size; x++) delete[] source[x];
delete[] target;
delete[] source;
return result;
}
int main(int argc, char** argv)
{
int num_cores = 4;
system("pause"); // So we can set cpu affinity before the benchmark starts
const int iters = 1000;
double avgElapsedTime = 0.0;
double elapsedTimes[iters];
for (int i = 0; i < iters; i++) {
LARGE_INTEGER frequency;
LARGE_INTEGER t1, t2;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&t1);
runBenchmark(num_cores);
QueryPerformanceCounter(&t2);
elapsedTimes[i] = (t2.QuadPart - t1.QuadPart) * 1000.0 / frequency.QuadPart;
avgElapsedTime += elapsedTimes[i];
}
avgElapsedTime = avgElapsedTime / iters;
double variance = 0;
for (int i = 0; i < iters; i++) {
variance += (elapsedTimes[i] - avgElapsedTime) * (elapsedTimes[i] - avgElapsedTime);
}
variance = sqrt(variance / iters);
printf("Average running time: %f, standard deviation: %f", avgElapsedTime, variance);
return 0;
}