Member 8464407 Ответов: 2

Замедление многопоточности, привязанное к процессору приложение, вероятно, связанное с аппаратным обеспечением


The exact details of my multi-theaded application probably don't matter. It's completely CPU bound (runs for hours or days at a time), does no I/O, there is ample memory for all the threads, and the threads barely communicate with each other (for all practical purposes they don't communicate with each other). There are many sub-problems to be solved by the computation, each of which runs for several hours, and the only purpose of multi-threading is to keep all the processor cores busy all the time. The threads do share some large blocks of memory, but the shared memory is only read and is never written into. Except for the sharing of the large block of memory which is only read, the application could just as easily be a separate "job" or "task" for each processor core.

Проблема в том, что процессорное время для решения каждой подзадачи увеличивается вместе с многопоточностью. Например, предположим, что существует небольшая тестовая задача, которая требует 250 секунд процессора для запуска в виде одного потока (а также 250 секунд настенных часов). Выполнение двух таких подзадач одновременно с двумя потоками может занять 270 секунд процессора для каждого потока в общей сложности 540 секунд процессора и 270 секунд настенных часов. В идеальном мире, где все "просто работает", тест двух подзадач одновременно потребовал бы 250 секунд процессора для каждого потока в общей сложности 500 секунд процессора и 250 секунд настенных часов.

Увеличение числа одновременных подзадач и потоков вплоть до числа процессорных ядер усугубляет кажущееся замедление. В целом всегда быстрее добавлять потоки до числа процессорных ядер, но использование n процессорных ядер в целом не приводит к ускорению в n раз. Например, в примере с 16 процессорными ядрами использование 16 потоков производит только примерно в 8 или 9 раз больше вычислений.

Очевидно, что существует какая-то аппаратная причина, такая как конфликт на шине памяти или конфликт между несколькими процессорными ядрами на одном процессорном чипе и т. д. Это известная проблема? Могу ли я что-нибудь с этим поделать?

Что я уже пробовал:

One possible problem is that I run some of the speed tests on a high end laptop with a single quad-core processor. Running all the processor cores at 100% generates heat which causes laptop power management to intervene and slow down the processor clock speeds because slower clock speeds generate less heat. I have tweaked power management settings to no avail. I also have an ancient server with 64GB memory and four quad-core processors, for a total of 16 cores. Running speed tests on this machine produce results that are essentially equivalent to the results on the laptop. So laptop heat and power management may be an issue, but if so it does not seem to be the main issue. Depending on the exact size of the speed test that I run, it can be slightly faster to run 16 separate jobs with one thread each or it can be slightly faster to run 1 big job with sixteen threads. The contention therefore does not seem to be in my application. Rather, it seems to be in the hardware. I run under Windows 10 on the laptop and under Wine under Linux on the server.

Kornfeld Eliyahu Peter

1. Управление потоками
2. Время ожидания в очереди
3. Блокировка между нитками за неправильной блокировки памяти
4. Начальный размер памяти код слишком мал, чтобы необходимость перераспределения
5. Блок обработки исключений
6. Вы пытаетесь обрабатывать потоки самостоятельно, без пула

Просто размышляю вслух...
Мы говорим о C++?

Peter_in_2780

Это почти наверняка ОС, а не аппаратное обеспечение. Даже если у вас не больше потоков, чем ядер, есть и другая фоновая активность, которая появляется и вызывает планировщик потоков ОС. То, что вы должны смотреть на это сродство процессоров (начните с Google), где вы можете прикрепить потоки к ядрам и сэкономить ОС, чтобы думать об этом. (В далеком туманном прошлом я потратил годы на настройку многопроцессорных систем.)

Dave Kreskowiak

Вы понимаете, что ваши потоки совместно используют процессор с O/S, который также имеет свои собственные потоки? Типичная Установка Windows имеет около 800+ запущенных потоков, 200 из которых находятся в самом ядре. Моя основная машина имеет около 1300 потоков, работающих после загрузки. Одна вкладка браузера Chrome-это 18 потоков.

Я знаю, что большинство потоков сидят без дела, но те, которые действительно работают, пусть и ненадолго, быстро складываются.

И вы задаетесь вопросом, почему вы не получаете линейного увеличения скорости для ваших 16 потоков или того, что вы используете?

Patrice T

Какую проблему вы решаете с помощью этих нитей ?

2 Ответов

Рейтинг:
1

Patrice T

Цитата:
Возможно, я наивен, но я действительно надеялся на почти 16-кратное ускорение на моем древнем сервере (Dell R900) с 16 процессорными ядрами.

ускорение может применяться только тогда, когда задача разделена между потоками, каждый из которых выполняет только часть работы параллельно.
Насколько я вижу, в вашем коде каждый поток выполняет полную работу.
Цитата:
Я действительно не верю, что моя программа имеет проблему многопоточного проектирования (но, конечно, это вполне возможно). Поэтому я решил попробовать создать полную фиктивную программу, которая иллюстрирует проблему и которая достаточно мала для публикации. Вот он.

В вашей программе каждый поток сортирует 10 000 000 массивов 10 раз. Этот вид деятельности разрушает кэш процессора. Таким образом, среда выполнения является doe для непрерывного промаха кэша, что не позволяет процессору использовать преимущества ядер.

Кстати, поток не делится памятью, каждый поток работает над своим собственным массивом из 10 000 000 и сортирует его 10 раз.


Member 8464407

Приношу свои извинения за то, что "ответил" на мой вопрос вместо того, чтобы "улучшить мой вопрос". Я пишу ассемблер и многопоточный код с 1960-х годов, но я новичок на этом конкретном форуме. (Теперь я пишу на C++ вместо ассемблера.)

Я думаю, что мне придется признать, что кэш процессора был разгромлен как наиболее вероятный виновник из - за отсутствия лучшей теории. Я, конечно, вижу, что мое приложение делает такие случайные обращения к памяти, что кэш процессора становится довольно сильно разбитым. По-прежнему трудно понять, почему дополнительные потоки так сильно усугубляют проблему, особенно на 16-ядерном компьютере, если я тестирую только с 3 или 4 потоками и если другие ядра полностью простаивают. Так что реальный вопрос - если разгром кэша-это проблема, то почему дополнительные потоки делают разгром кэша намного хуже?

Мое настоящее приложение действительно довольно параллельное. Каждый поток решает совершенно другую подзадачу. Вам не нужно иметь очереди и блокировки и тому подобное, чтобы иметь истинную параллельную обработку. Действительно, в моих ранних версиях этой программы было много очередей, блокировок, сигналов и других подобных межпотоковых коммуникаций. Это действительно замедляло работу программы, поэтому я переработал ее, чтобы избавиться от всего этого, и она действительно ускорилась. Все, что мне нужно было сделать, это сказать каждому потоку, над какой подзадачей работать, подождать несколько часов, пока поток будет работать над обозначенной подзадачей (это действительно большая проблема), а затем получить результаты обратно.

Я мог бы выполнять различную работу в каждом потоке фиктивной программы для выполнения теста времени вместо того, чтобы заставлять каждый поток выполнять одну и ту же работу. Но для теста синхронизации на самом деле не имело значения, что делали потоки, пока они жевали циклы и пока они дико обращались ко всему большому массиву, как это делает мое настоящее приложение.

Both my real app and the timing test do have a bunch of shared memory, and each thread of the real app and each thread of the timing test hits the shared memory pretty hard. Each thread does have its own local set of pointers to the shared memory and it's the pointers that get sorted. But if you look at the compare functor that's used with std::sort, it dereferences the pointers (iterators) and does its compare against the real data in the shared memory. The shared memory is read only so it's thread safe, but it does get used and it does get used very, very heavily. That has been a concern for me with this design. The trouble is that with the real problem, the shared memory is far and away the biggest consumer of memory in the whole program and I therefore really need it to be shared.

On the idea of affinity, I did try that suggestion. I know how with Windows to set affinity for separate instances of my program, but I can't figure out how with Windows to set affinity for individual threads. So I tested with setting affinity for multiple instances of my program where each instance was single threaded, and it seemed to make no difference. Extra instances increase the CPU time for each instance. I suspect that even with affinity, Windows could still be dispatching other threads from other Windows processes to "my" designated cores since those other threads from other Windows processes do not have affinity set to any particular core. It seems like what I really need is a way to tell Windows not to use "my" cores for anybody but me.

Patrice T

Приношу свои извинения за то, что "ответил" на мой вопрос вместо того, чтобы "улучшить мой вопрос".
Для меня это не проблема, все когда-то были новичками.
"если проблема заключается в разгроме кэша, то почему дополнительные потоки делают разгром кэша намного хуже?"
больше потоков - это все равно что уменьшить размер кэша каждого потока.

Для целей тестирования замените сортировку простым заполнением массива значением и посмотрите, получаете ли вы выгоду от многоядерности.

Рейтинг:
0

Member 8464407

Большое спасибо за ответы. Я думаю, мне нужно добавить немного дополнительной информации.

I am perhaps naive, but I really did hope for nearly a 16x speedup on my ancient server (Dell R900) with 16 processor cores. It's a Linux machine and my Windows program is running under Wine. The "top" command usually says that my program is getting 1580% to 1590% of the CPU out of a total of 1600%. There are certainly other processes running but they are taking very few cpu cycles. My program does no I/O, my threads do not interact (no queues, no locking, no waiting, etc.), and there is ample memory. (When I run on Windows, there is more "Windows junk" running in the background, but I can still usually get 350% percent or so of the machine for my program out of 400% possible with four processor cores.)

Я действительно не верю, что моя программа имеет проблему многопоточного проектирования (но, конечно, это вполне возможно). Поэтому я решил попробовать создать полную фиктивную программу, которая иллюстрирует проблему и которая достаточно мала для публикации. Вот он.

// timing_test.cpp : dummy program to test the timing of multiple CPU bound threads.
//

#include "stdafx.h"
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <boost/timer/timer.hpp>
#include <boost/thread.hpp>

	std::vector<int> numbers;	// vector to contain 10,000,000 numbers to be sorted as dummy work.
	void timing_test_2(void);	// function prototype for thread worker

		class Comp_modulus	// functor for comparing intergers mod k
	{
		public:

		int k;	// k value to calculate modulus

		bool operator()(const std::vector<int>::iterator &x, const std::vector<int>::iterator &y)
		{
			return ( ( (*x) % k ) < ( (*y) % k )  );	// compare the integers by their modulus rather than by their
														// .... values to sort in strange ways. It is iterators that
														// .... are being sorted, not the integers themselves.
		}
	};

int main(void)
{
	int N = 16;					// number of threads to create
	numbers.resize(10000000);	// storage for 10,000,000 numbers

	int i = 0;
	for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); it++)
		(*it) = i++; // fill in the numbers 0 to 9,999,999 which will be sorted in a variety of strange ways.

	std::cout << "beginning timing test, number of threads = " << std::setw(2) << (int)N << std::endl;	

	boost::timer::auto_cpu_timer auto_t;	// Quick and dirty overall timer. It prints automatically
											// .... when it is destructed at the end of the program.

	boost::thread_group tg;									// Define a thread group in preparation ....
															// .... for creating the threads	

	for (int i =  0; i < N; i++)
	{
		tg.create_thread( boost::bind( timing_test_2) );	// Create the N threads
	}

	tg.join_all();											// Wait until all the threads are done.

	std::cout << "completed timing test" << std::endl;

	return 0;
}

void timing_test_2(void)		// this is the thread that creates the artificial load to simulate work.
{
	std::vector<std::vector<int>::iterator> numbers_p;	// storage for iterators (pointers) which point to the array of integers
	numbers_p.resize( numbers.size() );					// allocate the correct amount of storage, same number of iterators
														// ... as integers to which they point.

	std::vector<int>::iterator it = numbers.begin();	// point to the beginning of the integer array.

	for (std::vector<std::vector<int>::iterator>::iterator it_it = numbers_p.begin(); it_it != numbers_p.end(); it_it++)
		(*it_it) = it++;	// populate the iterator vector with iterators pointing the the integers

	Comp_modulus comp_modulus;	// instantiate the functor which does strange compares

	// create the dummy work

	comp_modulus.k =  2; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k =  3; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k =  5; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k =  7; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 11; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 13; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 17; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 19; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 23; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );
	comp_modulus.k = 29; std::sort(numbers_p.begin(), numbers_p.end(), comp_modulus );

}


Как видите, это C++. Я компилирую на Windows с Visual Studio 2010. Поэтому я использую boost для некоторых функций синхронизации и многопоточности. Если у вас не установлен boost и если у вас есть достаточно новый компилятор C++, было бы просто выполнить синхронизацию и многопоточность с помощью стандартных библиотечных функций C++.

То, что делает моя тестовая программа, выглядит довольно странно, но на самом деле она довольно хорошо отражает то, что делает моя настоящая программа. Тестовая программа сначала создает массив (std::vector), содержащий 10 000 000 целых чисел в диапазоне от 0 до 9 999 999. Затем он создает искусственную рабочую нагрузку для целей синхронизации путем сортировки целых чисел по различным простым модулям. Например, сортировка по модулю 2 сортирует все четные числа перед всеми нечетными числами. Использование простых чисел для модулей гарантирует, что числа будут скремблированы довольно тщательно, и что std::sort имеет много работы, чтобы сделать.

Сортируемые числа хранятся только один раз и концептуально сортируются одновременно каждым потоком. Тем не менее, нет никакой блокировки между потоками, и числа на самом деле не перемещаются сортировкой вообще. Скорее, это волшебство достигается тем, что каждый поток создает свой собственный частный набор указателей на целые числа (итераторы в терминах стандартной библиотеки C++). Тогда сортируются указатели, а не целые числа.

В программе тестирования синхронизации целые числа 32-битные, а указатели 64-битные, поэтому каждый поток может также иметь свою собственную копию целых чисел и сортировать их напрямую, вместо того чтобы иметь указатели и сортировать программу. В моей реальной программе целые числа становятся 32-байтовыми перестановками, и потокам гораздо эффективнее работать с 64-битными указателями (8 байт), чем с 32-байтовыми перестановками. Кроме того, сортировка элементов длиной 8 байт выполняется намного быстрее, чем сортировка элементов длиной 32 байта.

Приложение-это вычислительная теория групп, и задача состоит в том, чтобы попытаться перечислить оптимальное решение для каждого из возможных положений Куба Рубика. Поиск оптимального решения для позиции является гораздо более вычислительно интенсивным, чем просто поиск решения для позиции. Существует около 4,3 х 10^19 позиций кубика Рубика, так что это чрезвычайно нетривиальная проблема. Это окончательное решение, несомненно, потребует многих сотен, если не тысяч или сотен тысяч машин, работающих над проблемой по частям.

Here are the results of running the dummy timing program on my ancient server. The test program is so simple minded that I had to run it 16 different times, changing the number of threads each time. You can see how the wall clock and cpu times do not scale up as you might hope. For example, with one thread the wall clock time and the cpu time are both about 24 seconds. So one would hope would be that with two threads the wall clock time would be about 24 seconds and that the cpu time would be about 48 seconds. But the real figures are about 26.5 seconds and about 53 seconds, respectively. The problem only gets worse with more threads. The same thing happens on my Windows machine. And the same thing happens if you get rid of the threads and just run multiple instances of a one thread program.

Джерри Брайан

beginning timing test, number of threads =  1
completed timing test
 24.109956s wall, 23.990000s user + 0.080000s system = 24.070000s CPU (99.8%)

beginning timing test, number of threads =  2
completed timing test
 26.558628s wall, 52.820000s user + 0.210000s system = 53.030000s CPU (199.7%)

beginning timing test, number of threads =  3
completed timing test
 27.632885s wall, 82.410000s user + 0.310000s system = 82.720000s CPU (299.4%)

beginning timing test, number of threads =  4
completed timing test
 33.766357s wall, 120.150000s user + 0.390000s system = 120.540000s CPU (357.0%)

beginning timing test, number of threads =  5
completed timing test
 34.820234s wall, 152.480000s user + 0.510000s system = 152.990000s CPU (439.4%)

beginning timing test, number of threads =  6
completed timing test
 33.279489s wall, 183.450000s user + 0.680000s system = 184.130000s CPU (553.3%)

beginning timing test, number of threads =  7
completed timing test
 35.896250s wall, 235.820000s user + 0.890000s system = 236.710000s CPU (659.4%)

beginning timing test, number of threads =  8
completed timing test
 36.156149s wall, 275.370000s user + 1.170000s system = 276.540000s CPU (764.8%)

beginning timing test, number of threads =  9
completed timing test
 38.529383s wall, 319.610000s user + 1.540000s system = 321.150000s CPU (833.5%)

beginning timing test, number of threads = 10
completed timing test
 40.542099s wall, 367.840000s user + 1.970000s system = 369.810000s CPU (912.2%)

beginning timing test, number of threads = 11
completed timing test
 45.129656s wall, 421.800000s user + 2.340000s system = 424.140000s CPU (939.8%)

beginning timing test, number of threads = 12
completed timing test
 44.265782s wall, 485.820000s user + 2.650000s system = 488.470000s CPU (1103.5%)

beginning timing test, number of threads = 13
completed timing test
 45.339959s wall, 545.310


Patrice T

Если это не является решением, откройте новый вопрос или
Воспользуйся Улучшить вопрос чтобы обновить ваш вопрос.
Чтобы каждый мог обратить внимание на эту информацию.