linux:多線程進程比單線程進程的性能要差?
問題解決:請見陳碩的回答,問題的最後有關於false sharing的參考文檔。
以下是正文**************************請原諒我的標題。
先說測試結論:特定情況下,在雙路多核伺服器環境下,多線程進程相對單線程進程下對性能的提升有限環境:2*Intel Xeon E5645 六核心,共24線程128GB 內存測試系統:REHL 6.3與 ubuntu server 14.04測試用例:單線程:隨機查詢含有500W元素的HASH表(很稀疏、加滿500W元素後占幾百MB內存),匹配欄位為一個int(預先生成一個隨機數組,所以不要懷疑是rand函數的原因);無鎖(無mutex、無spinlock);多線程:同上,每個線程單獨對應一個HASH,無衝突;
測試結果: 單線程進程每秒完成查詢500w次; 16線程進程每秒完成查詢1000w次,每個線程完成約62.5w次查詢 REHL和 ubuntu server表現基本相同。然而,為何我同時打開四個單線程進程就能達到超過1600w pps的性能?線程不是輕量級的進程嗎?PS:我在一台ubuntu desktop 12.04的台式機上(i5、4線程)多線程進程表現要好很多,4線程基本上是單線程的4倍。不好意思,讓大家久等了!先貼最新的測試結果:以下結果未設置CPU和thread的affanity,設置後有所提升,但差別不大。圖1鎖加的是pthread_mutex_t,可在代碼的「/***1.是否加鎖****/」下打開 圖2PC配置:1* Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz ubuntu desktop 12.04伺服器配置:聯想 2*Intel Xeon E5645 六核心,24線程 ubuntu server 14.04,在另外一台DELL的雙路六核伺服器上取得了相近的結果。如下是代碼,我對原有的程序進行了精簡,但不影響結果: 見我的回答
感謝諸位,在陳碩,3樓的匿名用戶、pig pig、teawater的建議下,我關掉了超線程、在石晉的建議下我使用numactl --cpubind=0 --membind=0 ./test_pthread設置node和內存的親緣性(numactl --hardware顯示兩個node,每個node對應8GB的內存,抱歉忘記截圖了)。另,不關超線程,單純設置CPU內存親緣性對性能提升有限。關掉伺服器的超線程後,結果如下: 圖3 圖4使用numactl --cpubind=0 --membind=0 ./test_pthread 啟動後,8線程下CPU情況如下:
額,目前還不知道如何設置才能利用好另外一路CPU。********************0926更新*******************感謝大牛陳碩,這個問題確是由false sharing引起的,在編寫這個demo的時候意識到這樣可能有問題,但沒有想到影響居然這麼大。另外,demo里加鎖不能起到保護作用,我只是順便測試一下不同鎖的性能,不要效仿。以上這段代碼中影響性能的主要是static volatile unsigned long op[100]和static volatile int g_rand = 0;其中前者造成了false sharing,而後者不應該使用、除非g_rand很少被修改。 若進行簡單修改(若不對請批評指正),static volatile unsigned long op[100]可以改為static volatile unsigned long op[100][N],其中N滿足N*sizeof(unsigned long) == 64,若嫌不保險,可以使N*sizeof(unsigned long) &> 128。關於false sharing的文檔有如下參考文獻:【1】Avoiding and Identifying False Sharing Among Threads
【2】Concurrency Hazards: False Sharing
貼代碼。目前猜測你看到的現象是Westmere和Sandy Bridge的區別。----有代碼就好說了,明顯是false sharing造成的問題。我把代碼修改了一下,基本去掉false sharing:
struct thread_info
{
pthread_t tid;
int id;
unsigned seed;
unsigned* hash;
int64_t count;
};
const int HASH_SIZE = 5 * 1024 * 1024;
const int64_t TOTAL_OPS = 1000 * 1000 * 1000;
double now()
{
struct timeval tv;
gettimeofday(tv, NULL);
return tv.tv_sec + tv.tv_usec / 1000000.0;
}
void* thread_func(void* arg)
{
struct thread_info ti = *(struct thread_info*)arg;
printf("%f thread: %d started
", now(), ti.id);
for (int64_t i = 0; i &< ti.count; ++i)
{
unsigned int obj = rand_r(ti.seed);
ti.hash[obj % HASH_SIZE] = obj; // rand_r(ti.seed);
}
printf("%f thread: %d finished
", now(), ti.id);
return NULL;
}
int main(int argc, char* argv[])
{
struct thread_info thread_infos[32];
bzero(thread_infos, sizeof thread_infos);
int num = sysconf(_SC_NPROCESSORS_CONF);
printf("system has %d processor(s)
", num);
srand(time(NULL));
int nThreads = argc &> 1 ? atoi(argv[1]) : 1;
double start = now();
printf("%f creating %d threads
", start, nThreads);
for (int i = 0; i &< nThreads; i++)
{
thread_infos[i].id = i;
thread_infos[i].seed = rand();
thread_infos[i].hash = calloc(HASH_SIZE, sizeof(unsigned int));
thread_infos[i].count = TOTAL_OPS;
pthread_create(thread_infos[i].tid, NULL, thread_func, thread_infos[i]);
}
for (int i = 0; i &< nThreads; i++)
{
pthread_join(thread_infos[i].tid, NULL);
}
double end = now();
double seconds = end - start;
printf("%f all threads stopped, elapsed %f seconds
", end, seconds);
printf("total_ops/s = %.3f M
ops/s for each thread = %.3f M
",
TOTAL_OPS / seconds / 1000000 * nThreads,
TOTAL_OPS / seconds / 1000000);
return 0;
}
在單路X5650,6核12線程上測試數據如下:
下次問這種問題先貼個profile的報告行不?
順便補一句,profile數據都沒看就猜來猜去的,基本上都是外行。你用的是Intel平台,oprofile,perf,itune,隨便哪個都能給你很好的結果,你們這樣猜來猜去是要鬧哪樣?個人猜測:並發線程過多緩存命中率下降。你有沒有試過單進程四線程?
第一是CACHE的問題 多TASK會有影響還有個問題CPU有硬線程 同一個核上的硬線程互相影響我也是觀察到過 你可以看看關掉硬線程 或者強制把TASK鎖到不同的核上 看看效果
實驗設計不夠清晰,得不到有效的結論,我覺得需要重新設計實驗。我能想到影響這種環境下多線程性能的影響因素,從體系結構的角度出發,大概有: SMT,緩存一致性, DVFS,緩存共享等方面。具體來說,E5645這個處理器通過HT的話才有12個線程,超線程出來的線程不能簡單視為的12個完全相同的硬體線程;緩存一致性來說,你的線程數目大於單個E5645的線程數目,基本可以確定線程會分布在兩個處理器上運行,這樣一致性的開銷將會變大;動態頻率調整,這個先不說了,和我正在做的研究相關;緩存共享,一方面假共享,另一方面就是對緩存資源的競爭。 如果真的想做對比,我的建議是,首先實現一個無鎖無同步的版本,禁用超線程和DVFS,線程和core進行綁定,線程數目不要超過6,實驗內容要包括多進程和多線程,n進程的性能應該會小於一個進程性能的n倍的,所以你比較的時候不能簡單的從一個進程推算n個進程。
我說的不一定對,不過我是這樣認為的:
我們的代碼編寫只是在高級語言層次,而不是彙編層次,經過代碼優化以及機器碼生成之後,線程之間是否存在互相鎖定,這在高級語言層次不完全可控。要寫出完全不互相影響的多線程代碼或許需要滿足很多特定條件。
線程之間需要互相共享內存空間,因此如果兩個 CPU 核心跑一個進程中的兩個線程時,對內存的訪問必然涉及到在兩個 CPU 核心之間的同步,這使得兩個 CPU 核心無法完全獨立的去執行任務。——所以單進程多線程必然將影響多核 CPU 的調度效率。
而多進程單線程就沒有這個問題,不同的CPU核心完全使用的不同的內存空間。
多線程是輕量級進程應該是僅僅對內存佔用而言的。但在主流的非同步框架中,線程與進程的數量都限制在了接近 CPU 核心數量的程度,如果只按照核心數量開進程,進程的內存佔用問題已經完全不再成為問題。同時對於多核 CPU 而言,多線程存在額外的負擔。在多核時代,或許進程才是更輕量級的存在。
就我個人的實踐而言,只在「絕對必要」的情況下才使用單進程多線程。否則都應當使用多進程。這不是Linux多線程或單線程的問題,而是題主自己的問題。
1.題主表述不清楚,出問題很正常。
題主沒表述清楚其使用什麼語言、環境、框架、IDE等。試想,多線程下的C++與Python,能一樣嗎?
題主沒表述HASH表的數據結構、內存申請方式、數據填充形式、匹配操作的流程細節、單線程的流程細節、多線程的流程細節等等。試想,就拿線程綁定CPU邏輯核來說,特殊情況下大部分線程都綁定到同一個核上,速度上的去嗎?
2.題主水平有硬傷,出問題很正常。
只讀式的匹配,是不需要鎖的。為什麼題主會提到【無鎖(無mutex、無spinlock)】?為什麼題主會提到【每個線程單獨對應一個HASH,無衝突】?
3.建議:
整個工程打包發上來,有什麼問題自然一目了然。1)上代碼2)perf 一下看看
各有優缺點及其適用的場景,起這樣的標題既不能肯定也不能否定,有亂扣帽子的嫌疑。
Memory wall (cpu快內存慢,瓶頸在內存). 可以設一下cpu和memory的affanity,可能會得到進一步的結論。
代碼:static volatile unsigned long op[100];static volatile int g_rand = 0;static volatile unsigned long pre_op[100];
/*用於測試的不同鎖*/
pthread_mutex_t g_mutex[100];
pthread_rwlock_t g_mutex_rw[100];
pthread_spinlock_t g_spinlock[100];
/*500W的Hash表*/
#define HASH_SIZE (5*1024*1024)
static unsigned int *g_hash[32];
/*測試線程
static void *thread_select(void *arg)
{
int i = * (int *)arg;
printf("thread:%d
",i);
cpu_set_t mask;
cpu_set_t get;
char buf[256];
int num = sysconf(_SC_NPROCESSORS_CONF);
printf("system has %d processor(s)
", num);
CPU_ZERO(mask);
CPU_SET(i, mask);
/*設置線程的CPU親緣性。
*/
// if (pthread_setaffinity_np(pthread_self(), sizeof(mask), mask) &< 0) {
// fprintf(stderr, "set thread affinity failed
");
// }
// unsigned int obj = rand();
while(1)
{
/***1.是否加鎖****/
// pthread_mutex_lock(g_mutex[i]);
// pthread_spin_lock(g_spinlock[i]);
// pthread_rwlock_rdlock(g_mutex_rw[i] );
// time(NULL);
// rand();
// 操作計數
op[i] ++;
unsigned int obj = g_rand;
memcmp(obj, g_hash[i] + (g_rand % HASH_SIZE ) , sizeof(unsigned int ) );
// pthread_rwlock_unlock(g_mutex_rw[i] );
// pthread_spin_unlock(g_spinlock[i]);
// pthread_mutex_unlock(g_mutex[i]);
}
}
/*列印線程*/
void *thread_print(void *arg)
{
int n;
while(1)
{
/*列印前四個線程當前總的操作計數*/
// printf("op[3]:%lu
",op[3]);
printf("thread0_op_times: %lu pps thread1_op_times: %lu pps thread2_op_times: %lu pps thread3_op_times: %lu pps
", op[0], op[1] ,op[2], op[3] );
unsigned long all_op = 0;
int j = 0;
for(j = 0 ;j &< 32; j ++)
{
// pthread_mutex_lock(g_mutex[j]);
all_op += (op[j] - pre_op[j]);
pre_op[j] = op[j];
// pthread_mutex_unlock(g_mutex[j]);
}
/*列印休眠周期內的總操作次數*/
printf("all_op:%lu
", all_op );
all_op = 0;
/*休眠*/
sleep(10);
// usleep(1000 * 990);
}
return NULL;
}
void *thread_rand(void *arg)
{
while(1)
{
g_rand = rand();
}
}
int main(void)
{
pthread_t t[40];
int i = 0;
int thread_id[32];
int total_num = 0;
/*初始化隨機數種子
*/
srand(time(NULL));
/*初始化鎖與計數
*/
/*啟動隨機數生成線程*/
pthread_create(t[34], NULL, thread_rand, NULL);
for( i = 0 ; i &< 32; i ++)
{
op[i] = 0;
pre_op[i] = 0;
pthread_mutex_init(g_mutex[i],NULL);
pthread_rwlock_init(g_mutex_rw[i], NULL);
pthread_spin_init(g_spinlock[i],0);
g_hash[i] = (unsigned int *)malloc( sizeof(unsigned int ) * HASH_SIZE );
memset(g_hash[i],0, sizeof(unsigned int ) * HASH_SIZE );
}
printf("start create thread_select
");
/*線程優先順序*/
struct sched_param param;
param.__sched_priority = 20;
pthread_attr_t attr;
pthread_attr_init(attr);
pthread_attr_setinheritsched(attr,PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(attr,SCHED_FIFO);
pthread_attr_setschedparam(attr,param);
/*啟動列印線程*/
pthread_create(t[35], NULL, thread_print, NULL);
/*線程編號*/
for( i=0;i&<32;i++){
thread_id[i]=i;
}
/***在這裡修改線程數****/
for(i=0;i &< 1;i++){
/*是否設置線程的優先順序*/
pthread_create(t[i], NULL, thread_select, (void *)thread_id[i]);
//// pthread_create(t[i], attr, thread_select, (void *)thread_id[i]);
}
while(1) {
sleep(1);
}
return 0;
}
昨晚睡得晚,現在起來看到答案有點醉……樓主啊,rand()函數不是並發的……多線訪問是有鎖的……額外的這個開銷導致你看到的狀態……
1、多核之間同步一級二級三級緩存數據;2、多線程的上下文切換;3、緩存行數據的競爭;
多線程對臨界變數加入多少鎖,然後還各種wait
實驗做得不夠清晰,樓主應該對比的是同樣N個線程,放在N個進程內和放在一個進程內的區別,建議做一系列的N,拿出數據再來討論。
另外,linux以線程為單位調度,想來應該不會有什麼區別。如果線程間沒有鎖的話應該是一樣的
隨機訪問,所以沒cache什麼事。Thread共用一個memory space。Process每個有單獨的memory space。memory也要一個一個去supply data,所以4個人做事肯定要比1個人快阿。
推薦閱讀:
※多線程無並發用到共享資源,還需要加鎖嗎?
※單線程中加鎖代碼的性能如何?
※redis為什麼是單線程?在多核處理器下對主存的訪問真的比多線程更有效率?未來有可能改用多線程嗎?
※malloc和free是線程安全的嗎,在多線程開發時用這兩個函數應該注意什麼?
※Python有GIL為什麼還需要線程同步?