8

マルチスレッド方式でいくつかの統計カウンターをインクリメントする必要があるアプリケーションがあります。インクリメントはスレッドセーフである必要があるため、gccアトミック組み込み__sync_add_and_fetch()関数を使用することにしました。それらの影響を理解するために、いくつかの簡単なパフォーマンステストを行ったところ、これらの関数は単純な前後のインクリメントよりもはるかに遅いことに気付きました。

これが私が作成したテストプログラムです:

#include <iostream>
#include <pthread.h>
#include <time.h>

using namespace std;

uint64_t diffTimes(struct timespec &start, struct timespec &end)
{
  if(start.tv_sec == end.tv_sec)
  {
    return end.tv_nsec - start.tv_nsec;
  }
  else if(start.tv_sec < end.tv_sec)
  {
    uint64_t nsecs = (end.tv_sec - start.tv_sec) * 1000000000;
    return nsecs + end.tv_nsec - start.tv_nsec;
  }
  else
  {
    // this is actually an error
    return 0;
  }
}

void outputResult(const char *msg, struct timespec &start, struct timespec &end, uint32_t numIterations, uint64_t val)
{
  uint64_t diff = diffTimes(start, end);
  cout << msg << ": "
       << "\n\t iterations: " << numIterations
       << ", result: " << val
       << "\n\t times [start, end] =  [" << start.tv_sec << ", " << start.tv_nsec << "]"
       << "\n\t [" << end.tv_sec << ", " << end.tv_nsec << "]"
       << "\n\t [total, avg] = [" << diff
       << ", " << (diff/numIterations) << "] nano seconds"
       << endl;
}

int main(int argc, char **argv)
{
  struct timespec start, end;
  uint64_t val = 0;
  uint32_t numIterations = 1000000;

  //
  // NON ATOMIC pre increment
  //
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    ++val;
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("Non-Atomic pre-increment", start, end, numIterations, val);
  val = 0;

  //
  // NON ATOMIC post increment
  //
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    val++;
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("Non-Atomic post-increment", start, end, numIterations, val);
  val = 0;

  //
  // ATOMIC add and fetch
  //
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    __sync_add_and_fetch(&val, 1);
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("Atomic add and fetch", start, end, numIterations, val);
  val = 0;

  //
  // ATOMIC fetch and add
  //
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    __sync_fetch_and_add(&val, 1);
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("Atomic fetch and add", start, end, numIterations, val);
  val = 0;

  //
  // Mutex protected post-increment
  //
  pthread_mutex_t mutex;
  pthread_mutex_init(&mutex, NULL);
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    pthread_mutex_lock(&mutex);
    val++;
    pthread_mutex_unlock(&mutex);
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("Mutex post-increment", start, end, numIterations, val);
  val = 0;

  //
  // RWlock protected post-increment
  //
  pthread_rwlock_t rwlock;
  pthread_rwlock_init(&rwlock, NULL);
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
  for(uint32_t i = 0; i < numIterations; ++i)
  {
    pthread_rwlock_wrlock(&rwlock);
    val++;
    pthread_rwlock_unlock(&rwlock);
  }
  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);

  outputResult("RWlock post-increment", start, end, numIterations, val);
  val = 0;

  return 0;
}

そしてここに結果があります:

# ./atomicVsNonAtomic
Non-Atomic pre-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 1585375]
         [0, 1586185]
         [total, avg] = [810, 0] nano seconds
Non-Atomic post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 1667489]
         [0, 1667920]
         [total, avg] = [431, 0] nano seconds
Atomic add and fetch:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 1682037]
         [0, 16595016]
         [total, avg] = [14912979, 14] nano seconds
Atomic fetch and add:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 16617178]
         [0, 31499571]
         [total, avg] = [14882393, 14] nano seconds
Mutex post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 31526810]
         [0, 68515763]
         [total, avg] = [36988953, 36] nano seconds
RWlock post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 68547649]
         [0, 110877351]
         [total, avg] = [42329702, 42] nano seconds

これがgccコンパイルです:

g++ -o atomicVsNonAtomic.o -c -march=i686 -O2 -I. atomicVsNonAtomic.cc
g++ -o atomicVsNonAtomic atomicVsNonAtomic.o -lrt -lpthread

および関連情報とバージョン:

# gcc --version
gcc (GCC) 4.3.2
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

# uname -a
Linux gtcba2v1 2.6.32.12-0.7-default #1 SMP 2010-05-20 11:14:20 +0200 i686 i686 i386 GNU/Linux

そして今、実際の質問です:)アトミック操作が非常に遅いのは正常ですか?

100万回の反復の違いは次のとおりです。

  • 単純なポストインクリメント:431ナノ秒
  • アトミックフェッチアンドアッド操作:14882393ナノ秒

もちろん、不可分操作はもっとコストがかかるはずだと私は理解していますが、これは誇張されているようです。完全を期すために、pthreadミューテックスとrwlockもチェックしました。少なくともアトミック操作はpthread操作よりも高速ですが、私はまだ何か間違ったことをしたのではないかと思っています。-march=i686オプションを指定せずにリンクさせることができませんでした。おそらくこれは影響がありますか?

アップデート:

私は-O2コンパイラの最適化を取り除いて、次のようにより一貫した結果を得ることができました:

# ./atomicVsNonAtomic
Non-Atomic pre-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 1647303]
         [0, 4171164]
         [total, avg] = [2523861, 2] nano seconds
Non-Atomic post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 4310230]
         [0, 7262704]
         [total, avg] = [2952474, 2] nano seconds
Atomic add and fetch:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 7285996]
         [0, 25919067]
         [total, avg] = [18633071, 18] nano seconds
Atomic fetch and add:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 25941677]
         [0, 44544234]
         [total, avg] = [18602557, 18] nano seconds
Mutex post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 44573933]
         [0, 82318615]
         [total, avg] = [37744682, 37] nano seconds
RWlock post-increment:
         iterations: 1000000, result: 1000000
         times [start, end] =  [0, 82344866]
         [0, 125124498]
         [total, avg] = [42779632, 42] nano seconds
4

3 に答える 3

20

答えは、GCCが非アトミックな増分を最適化することです。次のようなループが表示された場合:

for (int i=0; i<N; i++) x++;

次のように置き換えます。

x += N;

これは、生成されたアセンブリで確認できます。これには、次のものが含まれます。

call    clock_gettime
leal    -32(%ebp), %edx
addl    $1000000, -40(%ebp)     <- increment by 1000000
adcl    $0, -36(%ebp)
movl    %edx, 4(%esp)
movl    $2, (%esp)
call    clock_gettime

ですから、あなたは自分が何であるかを測定しているのではありません。

volatileこの最適化を防ぐために変数を作成できます。私のコンピューターでは、これを行った後、非アトミックアクセスはアトミックアクセスの約8倍の速度になります。64ビットの代わりに32ビット変数を使用する場合(私は32ビットとしてコンパイルしています)、差は約3分の1に低下します。

于 2012-07-23T08:40:18.923 に答える
6

gccが非アトミックインクリメント操作を次のようなものに最適化していると思います

val += numIterations;

10 ^ 6の増分には431ナノ秒かかると言います。これは、ループの反復ごとに0.000431nsになります。4 GHzプロセッサでは、クロックサイクルは0.25 nsであるため、ループが最適化されていることは明らかです。これは、あなたが見ている大きなパフォーマンスの違いを説明しています。

編集:アトミック操作を14 nsと測定しました。これも、4 GHzプロセッサを想定すると、56サイクルになりますが、これはかなり適切です。

于 2012-07-23T08:40:38.847 に答える
1

同期メカニズムの速度は、単一のスレッドでは測定できません。POSIXミューテックス/Windowsクリティカルセクションのような単一プロセスの同期オブジェクトは、競合する場合にのみ実際に時間がかかります。

同期されたメソッドがどのくらいの時間がかかるかを実際に把握するには、いくつかのスレッドを導入する必要があります-実際のアプリケーションの時間を反映する他の作業を行います-。

于 2012-07-23T09:35:10.870 に答える