나는 (파이썬 확장 내에서) 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;
}
입자 수는 N = 100에서 100,000까지이며 적어도 그 수를 필요로합니다. 아마도 10N과 같은 것입니다. 큰 숫자의 경우 timesteps는 최대 1 초가 걸리고 평형에 도달하기 위해 50,000 개를 실행해야 할 때 ... :) 감사합니다. 그러나 라운드 로빈은 큰 뚱뚱한 무작위 계산없이 잠금을 최소화하기위한 트릭을 수행 할 수 있습니다. – tehwalrus
당신은 약간의 프로파일 링을 할 수 있지만, 일반적으로 rand()와 같은 의사 난수 생성기 (이것은 충분히 유용 할 것입니다)는 실제로 계산이 많이 필요하지 않습니다. 두 곱셈을 사용하여 구현할 수 있으며 결과를 오버플로하거나 피드백 시프트 레지스터를 사용하여 추가 할 수 있습니다. 암호화 랜덤 제너레이터는 중량이있을 수 있지만 확실히 필요하지는 않습니다. – TJD
실제로 라운드 로빈은 다른 잠금 동작의 필요성을 줄여 주며 한 번에 하나의 스레드를 방해하지 않고 작업 대기열을 유지합니다. 그것을 구현하는 것의 절반 방법이지만 하나 이상의 스레드가/: 곧 알게 될 때 좌절감을주는 segfaults를 얻습니다! :) – tehwalrus