5

私は、atomic_add_64 対ミューテックス ロック アプローチを使用して、64 ビット値の単純なアトミック インクリメントのパフォーマンスを測定する単純なプログラムでいくつかのテストを行っていました。私を困惑させているのは、atomic_add がミューテックス ロックよりも 2 倍遅いことです。

編集!!!さらにいくつかのテストを行いました。アトミックはミューテックスよりも高速で、最大 8 つの同時スレッドに拡張できるようです。その後、アトミックのパフォーマンスは大幅に低下します。

私がテストしたプラットフォームは次のとおりです。

SunOS 5.10 Generic_141444-09 sun4u sparc SUNW、Sun-Fire-V490

CC: Sun C++ 5.9 SunOS_sparc パッチ 124863-03 2008/03/12

プログラムは非常に単純です。

#include <stdio.h>
#include <stdint.h>
#include <pthread.h>
#include <atomic.h>

uint64_t        g_Loops = 1000000;
volatile uint64_t       g_Counter = 0;
volatile uint32_t       g_Threads = 20;

pthread_mutex_t g_Mutex;
pthread_mutex_t g_CondMutex;
pthread_cond_t  g_Condition;

void LockMutex() 
{ 
  pthread_mutex_lock(&g_Mutex); 
}

void UnlockMutex() 
{ 
   pthread_mutex_unlock(&g_Mutex); 
}

void InitCond()
{
   pthread_mutex_init(&g_CondMutex, 0);
   pthread_cond_init(&g_Condition, 0);
}

void SignalThreadEnded()
{
   pthread_mutex_lock(&g_CondMutex);
   --g_Threads;
   pthread_cond_signal(&g_Condition);
   pthread_mutex_unlock(&g_CondMutex);
}

void* ThreadFuncMutex(void* arg)
{
   uint64_t counter = g_Loops;
   while(counter--)
   {
      LockMutex();
      ++g_Counter;
      UnlockMutex();
   }
   SignalThreadEnded();
   return 0;
}

void* ThreadFuncAtomic(void* arg)
{
   uint64_t counter = g_Loops;
   while(counter--)
   {
      atomic_add_64(&g_Counter, 1);
   }
   SignalThreadEnded();
   return 0;
}


int main(int argc, char** argv)
{
   pthread_mutex_init(&g_Mutex, 0);
   InitCond();
   bool bMutexRun = true;
   if(argc > 1)
   {
      bMutexRun = false;
      printf("Atomic run!\n");
   }
   else
        printf("Mutex run!\n");

   // start threads
   uint32_t threads = g_Threads;
   while(threads--)
   {
      pthread_t thr;
      if(bMutexRun)
         pthread_create(&thr, 0,ThreadFuncMutex, 0);
      else
         pthread_create(&thr, 0,ThreadFuncAtomic, 0);
   }
   pthread_mutex_lock(&g_CondMutex);
   while(g_Threads)
   {
      pthread_cond_wait(&g_Condition, &g_CondMutex);
      printf("Threads to go %d\n", g_Threads);
   }
   printf("DONE! g_Counter=%ld\n", (long)g_Counter);
}

ボックスでのテスト実行の結果は次のとおりです。

$ CC -o atomictest atomictest.C
$ time ./atomictest
Mutex run!
Threads to go 19
...
Threads to go 0
DONE! g_Counter=20000000

real    0m15.684s
user    0m52.748s
sys     0m0.396s

$ time ./atomictest 1
Atomic run!
Threads to go 19
...
Threads to go 0
DONE! g_Counter=20000000

real    0m24.442s
user    3m14.496s
sys     0m0.068s

Solaris でこの種のパフォーマンスの違いに遭遇しましたか? なぜこれが起こるのですか?

Linux では、同じコード (gcc __sync_fetch_and_add を使用) により、mutex バージョンの 5 倍のパフォーマンス向上が得られます。

ありがとう、オクタフ

4

1 に答える 1

4

ここで何が起こっているのか注意する必要があります。

  1. スレッドの作成にはかなりの時間がかかります。したがって、すべてのスレッドが同時に実行されているわけではありません。証拠として、私はあなたのコードを取得してミューテックス ロックを削除し、実行するたびに正しい答えを得ました。これは、どのスレッドも同時に実行されていないことを意味します! テストでスレッドを作成/破棄する時間を数えるべきではありません。テストを開始する前に、すべてのスレッドが作成されて実行されるまで待つ必要があります。

  2. あなたのテストは公平ではありません。あなたのテストには、人為的に非常に高いロック競合があります。何らかの理由で、アトミックな add_and_fetch はその状況で苦しんでいます。実生活では、スレッドで何らかの作業を行います。少しでも作業を追加すると、アトミック ops のパフォーマンスが大幅に向上します。これは、競合状態の可能性が大幅に低下したためです。競合がない場合、アトミック op のオーバーヘッドは低くなります。競合がない場合、mutex にはアトミック op よりも多くのオーバーヘッドがあります。

  3. スレッド数。実行中のスレッドが少ないほど、競合は少なくなります。これが、このテストでアトミックに適したスレッドが少ない理由です。8 スレッド数は、システムがサポートする同時スレッド数である可能性があります。あなたのテストが競合に偏っていたからではないかもしれません。あなたのテストは、許可された同時スレッドの数にスケーリングされ、その後プラトーになるように思えます。私が理解できないことの 1 つは、スレッドの数がシステムが処理できる同時スレッドの数よりも多くなると、スレッドがスリープしている間にミューテックスがロックされたままになるという状況の証拠が見られない理由です。多分私たちはそうするでしょう、私はそれが起こっているのを見ることができません。

要するに、アトミックはほとんどの現実の状況ではるかに高速です。ロックを長時間保持しなければならない場合、それらはあまり良くありません...とにかく避けるべきものです(少なくとも私の意見では!)

私はあなたのコードを変更して、スレッド数を変更するだけでなく、作業なし、ほとんど作業なし、もう少し作業を加えてテストできるようにしました。

6sm = 6 スレッド、ほとんど作業なし、mutex 6s = 6 スレッド、ほとんど作業なし、アトミック

キャピトル S を使用してより多くの作業を取得し、no s を使用して作業を取得しません。

これらの結果は、10 スレッドの場合、作業量がアトミックの速度に影響することを示しています。最初のケースでは、作業はなく、アトミックはかろうじて高速です。少し作業を追加すると、ギャップが 2 倍の 6 秒になり、多くの作業を追加すると、ほぼ 10 秒になります。

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=10; a.out $t ; a.out "$t"m
ATOMIC FAST g_Counter=10000000 13.6520 s
MUTEX  FAST g_Counter=10000000 15.2760 s

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=10s; a.out $t ; a.out "$t"m
ATOMIC slow g_Counter=10000000 11.4957 s
MUTEX  slow g_Counter=10000000 17.9419 s

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=10S; a.out $t ; a.out "$t"m
ATOMIC SLOW g_Counter=10000000 14.7108 s
MUTEX  SLOW g_Counter=10000000 23.8762 s

20 スレッド、アトミックはさらに優れていますが、マージンは小さくなっています。動作しません。ほぼ同じ速度です。多くの作業により、アトミックが再び主導権を握ります。

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=20; a.out $t ; a.out "$t"m
ATOMIC FAST g_Counter=20000000 27.6267 s
MUTEX  FAST g_Counter=20000000 30.5569 s

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=20S; a.out $t ; a.out "$t"m
ATOMIC SLOW g_Counter=20000000 35.3514 s
MUTEX  SLOW g_Counter=20000000 48.7594 s

2 スレッド。アトミックが優勢。

(2) /dev_tools/Users/c698174/temp/atomic 
[c698174@shldvgfas007] $ t=2S; a.out $t ; a.out "$t"m
ATOMIC SLOW g_Counter=2000000 0.6007 s
MUTEX  SLOW g_Counter=2000000 1.4966 s

コードは次のとおりです (redhat linux、gcc アトミックを使用):

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <pthread.h>

volatile uint64_t __attribute__((aligned (64))) g_Loops = 1000000 ;
volatile uint64_t __attribute__((aligned (64))) g_Counter = 0;
volatile uint32_t __attribute__((aligned (64))) g_Threads = 7; 
volatile uint32_t __attribute__((aligned (64))) g_Active = 0;
volatile uint32_t __attribute__((aligned (64))) g_fGo = 0;
int g_fSlow = 0;

#define true 1
#define false 0
#define NANOSEC(t) (1000000000ULL * (t).tv_sec + (t).tv_nsec)

pthread_mutex_t g_Mutex;
pthread_mutex_t g_CondMutex;
pthread_cond_t  g_Condition;

void LockMutex() 
{ 
  pthread_mutex_lock(&g_Mutex); 
}

void UnlockMutex() 
{ 
   pthread_mutex_unlock(&g_Mutex); 
}

void Start(struct timespec *pT)
{
   int cActive = __sync_add_and_fetch(&g_Active, 1);
   while(!g_fGo) {} 
   clock_gettime(CLOCK_THREAD_CPUTIME_ID, pT);
}

uint64_t End(struct timespec *pT)
{
   struct timespec T;
   int cActive = __sync_sub_and_fetch(&g_Active, 1);
   clock_gettime(CLOCK_THREAD_CPUTIME_ID, &T);
   return NANOSEC(T) - NANOSEC(*pT);
}
void Work(double *x, double z)
{
      *x += z;
      *x /= 27.6;
      if ((uint64_t)(*x + .5) - (uint64_t)*x != 0)
        *x += .7;
}
void* ThreadFuncMutex(void* arg)
{
   struct timespec T;
   uint64_t counter = g_Loops;
   double x = 0, z = 0;
   int fSlow = g_fSlow;

   Start(&T);
   if (!fSlow) {
     while(counter--) {
        LockMutex();
        ++g_Counter;
        UnlockMutex();
     }
   } else {
     while(counter--) {
        if (fSlow==2) Work(&x, z);
        LockMutex();
        ++g_Counter;
        z = g_Counter;
        UnlockMutex();
     }
   }
   *(uint64_t*)arg = End(&T);
   return (void*)(int)x;
}

void* ThreadFuncAtomic(void* arg)
{
   struct timespec T;
   uint64_t counter = g_Loops;
   double x = 0, z = 0;
   int fSlow = g_fSlow;

   Start(&T);
   if (!fSlow) {
     while(counter--) {
        __sync_add_and_fetch(&g_Counter, 1);
     }
   } else {
     while(counter--) {
        if (fSlow==2) Work(&x, z);
        z = __sync_add_and_fetch(&g_Counter, 1);
     }
   }
   *(uint64_t*)arg = End(&T);
   return (void*)(int)x;
}


int main(int argc, char** argv)
{
   int i;
   int bMutexRun = strchr(argv[1], 'm') != NULL;
   pthread_t thr[1000];
   uint64_t aT[1000];
   g_Threads = atoi(argv[1]);
   g_fSlow = (strchr(argv[1], 's') != NULL) ? 1 : ((strchr(argv[1], 'S') != NULL) ? 2 : 0);

   // start threads
   pthread_mutex_init(&g_Mutex, 0);
   for (i=0 ; i<g_Threads ; ++i)
         pthread_create(&thr[i], 0, (bMutexRun) ? ThreadFuncMutex : ThreadFuncAtomic, &aT[i]);

   // wait
   while (g_Active != g_Threads) {}
   g_fGo = 1;
   while (g_Active != 0) {}

   uint64_t nTot = 0;
   for (i=0 ; i<g_Threads ; ++i)
   { 
        pthread_join(thr[i], NULL);
        nTot += aT[i];
   }
   // done 
   printf("%s %s g_Counter=%llu %2.4lf s\n", (bMutexRun) ? "MUTEX " : "ATOMIC", 
    (g_fSlow == 2) ? "SLOW" : ((g_fSlow == 1) ? "slow" : "FAST"), g_Counter, (double)nTot/1e9);
}
于 2012-04-28T04:37:13.283 に答える