2011-11-29 3 views
0

나는 (파이썬 확장 내에서) C로 작업 대기열 패턴을 구현했으며 성능에 실망합니다.파이썬 C 확장 : 다중 스레드 및 난수

필자는 파티클 목록 ("요소")이있는 시뮬레이션을 사용하며, 타임 스텝에 필요한 모든 계산을 수행하는 데 걸리는 시간을 벤치마킹하여 관련 입자 수와 함께 기록합니다. 나는 쿼드 코어 하이퍼 스레딩 i7에서 코드를 실행하고 있기 때문에 최대 8 개의 스레드로 성능이 향상 될 것으로 기대하고 있지만 가장 빠른 구현에는 작업자 스레드가 없습니다 (기능은 간단합니다. 큐에 추가하는 대신 실행 됨), 각 작업자 스레드에서 코드가 느려지고 느려집니다 (각 새 스레드에 대한 스레드되지 않은 구현 시간보다 더 많은 단계로!). 내 프로세서 사용량을 빠르게 살펴 보았습니다 응용 프로그램 및 파이썬 실제로 얼마나 많은 스레드가 실행중인 관계없이 130 % CPU 사용량을 초과하지 않는 것. 머신은 그 이상의 많은 헤드 룸을 가지고 있으며 전체 시스템 사용량은 약 200 %입니다. 각 작업 항목의 실행이 두 가지 요소에 대한 잠금을 필요로하며, 유사한 요소가 큐에 서로 가까이되기 때문에

(아래 그림 참조) 내 큐 구현의 지금 부분은 대기열에서 임의에서 항목 을 선택하고있다. 따라서 스레드가 임의의 인덱스를 선택하고 큐의 다른 비트를 공격하여 뮤텍스 충돌을 최소화하려고합니다.

이제

, 나는 (그 문장 메이크업 감각? 확실하지 ... 않음)

내가 해봤 내 임의의 숫자는 스레드로부터 안전하지 않았기 때문에 rand() 내 최초의 시도가 느리다 것이다 읽었습니다 구현은 random()drand48_r (비록 불행히도, 후자는 OS X에서는 사용할 수없는 것 같습니다)와 함께 통계를 사용하지 마십시오.

아마도 다른 사람이 문제의 원인을 알 수 있습니까? 코드 (작업자 함수)는 아래에 있으며, queue_add 함수 나 생성자 중 하나라도 유용하다고 생각되면 큰 소리로 외칩니다.

void* worker_thread_function(void* untyped_queue) { 

    queue_t* queue = (queue_t*)untyped_queue; 
    int success = 0; 
    int rand_id; 
    long int temp; 
    work_item_t* work_to_do = NULL; 
    int work_items_completed = 0; 

    while (1) { 
    if (pthread_mutex_lock(queue->mutex)) { 

     // error case, try again: 
     continue; 
    } 

    while (!success) { 

     if (queue->queue->count == 0) { 

     pthread_mutex_unlock(queue->mutex); 
     break; 
     } 

     // choose a random item from the work queue, in order to avoid clashing element mutexes. 
     rand_id = random() % queue->queue->count; 

     if (!pthread_mutex_trylock(((work_item_t*)queue->queue->items[rand_id])->mutex)) { 

     // obtain mutex locks on both elements for the work item. 
     work_to_do = (work_item_t*)queue->queue->items[rand_id]; 

     if (!pthread_mutex_trylock(((element_t*)work_to_do->element_1)->mutex)){ 
      if (!pthread_mutex_trylock(((element_t*)work_to_do->element_2)->mutex)) { 

      success = 1; 
      } else { 

      // only locked element_1 and work item: 
      pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex); 
      pthread_mutex_unlock(work_to_do->mutex); 
      work_to_do = NULL; 
      } 
     } else { 

      // couldn't lock element_1, didn't even try 2: 
      pthread_mutex_unlock(work_to_do->mutex); 
      work_to_do = NULL; 
     } 
     } 
    } 

    if (work_to_do == NULL) { 
     if (queue->queue->count == 0 && queue->exit_flag) { 

     break; 
     } else { 

     continue; 
     } 
    } 

    queue_remove_work_item(queue, rand_id, NULL, 1); 
    pthread_mutex_unlock(work_to_do->mutex); 

    pthread_mutex_unlock(queue->mutex); 

    // At this point, we have mutex locks for the two elements in question, and a 
    // work item no longer visible to any other threads. we have also unlocked the main 
    // shared queue, and are free to perform the work on the elements. 
    execute_function(
     work_to_do->interaction_function, 
     (element_t*)work_to_do->element_1, 
     (element_t*)work_to_do->element_2, 
     (simulation_parameters_t*)work_to_do->params 
    ); 

    // now finished, we should unlock both the elements: 
    pthread_mutex_unlock(((element_t*)work_to_do->element_1)->mutex); 
    pthread_mutex_unlock(((element_t*)work_to_do->element_2)->mutex); 

    // and release the work_item RAM: 
    work_item_destroy((void*)work_to_do); 
    work_to_do = NULL; 

    work_items_completed++; 
    success = 0; 
    } 
    return NULL; 
} 

답변

0

스레드 수에 관계없이 동일한 코드이므로 random()이 문제가 아닌 것 같습니다. 스레드 수가 많아지면 성능이 저하되므로 오버 헤드를 잠그면 죽을 수도 있습니다. 다중 스레드가 정말로 필요합니까? 업무 기능이 얼마나 오래 걸리고 평균 대기열 깊이는 얼마입니까? 무작위로 항목을 선택하는 것은 나쁜 생각처럼 보입니다. 확실히 대기열 수가 < = 2 인 경우에는 rand 계산을 수행 할 필요가 없습니다. 또한 임의로 큐 인덱스를 선택하는 대신 작업자 스레드마다 다른 큐를 사용하고 라운드 로빈 방식으로 삽입하는 것이 좋습니다. 또는, 적어도 이전 스레드가 요청한 마지막 인덱스를 기억하고 그 인덱스를 선택하지 않는 것과 같은 간단한 것.

+0

입자 수는 N = 100에서 100,000까지이며 적어도 그 수를 필요로합니다. 아마도 10N과 같은 것입니다. 큰 숫자의 경우 timesteps는 최대 1 초가 걸리고 평형에 도달하기 위해 50,000 개를 실행해야 할 때 ... :) 감사합니다. 그러나 라운드 로빈은 큰 뚱뚱한 무작위 계산없이 잠금을 최소화하기위한 트릭을 수행 할 수 있습니다. – tehwalrus

+0

당신은 약간의 프로파일 링을 할 수 있지만, 일반적으로 rand()와 같은 의사 난수 생성기 (이것은 충분히 유용 할 것입니다)는 실제로 계산이 많이 필요하지 않습니다. 두 곱셈을 사용하여 구현할 수 있으며 결과를 오버플로하거나 피드백 시프트 레지스터를 사용하여 추가 할 수 있습니다. 암호화 랜덤 제너레이터는 중량이있을 수 있지만 확실히 필요하지는 않습니다. – TJD

+0

실제로 라운드 로빈은 다른 잠금 동작의 필요성을 줄여 주며 한 번에 하나의 스레드를 방해하지 않고 작업 대기열을 유지합니다. 그것을 구현하는 것의 절반 방법이지만 하나 이상의 스레드가/: 곧 알게 될 때 좌절감을주는 segfaults를 얻습니다! :) – tehwalrus

0

파이썬 스레드는 실제 스레드가 아닙니다. 모든 파이썬 스레드는 동일한 OS 레벨 스레드에서 실행되며 GIL (Global Interpreter Lock) 덕분에 한 번에 하나씩 실행됩니다. 작업자가 컨텍스트에서 상대적으로 수명이 긴 경우 코드를 프로세스로 다시 작성하면 트릭을 수행 할 수 있습니다.

Wikipedia's page on GIL

---- 편집 ----

오른쪽이는 다했다. 그러나 GIL은 여전히 ​​중요합니다. Info on threads in c extensions

+0

기다려라. 파이썬은 pthread와 같은 시스템 C 라이브러리를 사용하는 방식을 망치고있다. 나는 C로 작성된 비트 중에 C 확장이 순수한 C 프로그램만큼 빠르다 고 생각했다. – tehwalrus

+1

pthreads를 제대로 막을 수 있는지 확실하지 않지만 일반적인 파이썬 스레드를 사용할 때와 똑같은 동작을합니다. 스레드에서 파이썬 객체를 만지지 않는 한, 문서에서 특정 내용이 표시되지 않습니다. http://docs.python.org/c-api/init.html#non-python-created-threads –

+0

흥미 롭습니다. 이 경우, 저는 이것을하지는 않을 것이지만, 제 코드의 섹션이있을 수 있습니다. 감사! – tehwalrus

0

이것이 프로그램의 병목 현상인지 알아 보려면 벤치 마크하고 확인해야하지만 가능할 수도 있습니다.

random() 및 숨겨진 상태 변수가있는 친구는 병렬 프로그래밍의 심각한 병목 현상이 될 수 있습니다. 스레드가 안전하게 만들어지면 일반적으로 액세스를 뮤텍스 처리하여 수행하므로 모든 것이 느려집니다.

POSIX 시스템에서 스레드 안전 임의 생성기에 대한 휴대용 선택은 erand48입니다. drand48과 대조적으로 상태 변수를 인수로받습니다. 각 스레드의 스택에 상태 변수 (unsigned short[3])를 유지하고이를 erand48으로 호출하면됩니다.

또한 의사 번호 임의 생성기입니다. 다른 스레드간에 동일한 상태 변수를 사용하면 임의의 숫자가 독립적이지 않습니다.