web-dev-qa-db-fra.com

Comment obtenir 100% d'utilisation du processeur d'un programme C

C'est une question assez intéressante alors laissez-moi vous décrire le contexte. Je travaille au Musée national de l'informatique, et nous venons juste de faire fonctionner un super ordinateur Cray Y-MP EL depuis 1992, et nous voulons vraiment voir à quelle vitesse il peut aller!

Nous avons décidé que la meilleure façon de procéder consistait à écrire un programme en C simple permettant de calculer les nombres premiers et de montrer le temps nécessaire, puis de l'exécuter sur un ordinateur de bureau moderne et rapide et de comparer les résultats.

Nous avons rapidement créé ce code pour compter les nombres premiers:

#include <stdio.h>
#include <time.h>

void main() {
    clock_t start, end;
    double runTime;
    start = clock();
    int i, num = 1, primes = 0;

    while (num <= 1000) { 
        i = 2; 
        while (i <= num) { 
            if(num % i == 0)
                break;
            i++; 
        }
        if (i == num)
            primes++;

        system("clear");
        printf("%d prime numbers calculated\n",primes);
        num++;
    }

    end = clock();
    runTime = (end - start) / (double) CLOCKS_PER_SEC;
    printf("This machine calculated all %d prime numbers under 1000 in %g seconds\n", primes, runTime);
}

Qui sur notre ordinateur portable dual core fonctionnant sous Ubuntu (The Cray utilise UNICOS), fonctionnait parfaitement, obtenant une utilisation de 100% du processeur et prenant environ 10 minutes. Quand je suis rentré chez moi, j'ai décidé de l'essayer sur mon PC de jeu moderne à cœur hexagonal, et c'est là que nous obtenons nos premiers problèmes.

J'ai d'abord adapté le code pour qu'il fonctionne sous Windows, car c'est ce qu'utilisait le PC de jeu, mais j'ai été attristé de constater que le processus ne récupérait que 15% environ de la puissance du processeur. J'ai pensé que cela devait être Windows étant Windows, alors j'ai démarré sur un Live CD d'Ubuntu en pensant qu'Ubuntu permettrait au processus de fonctionner avec tout son potentiel, comme il l'avait fait auparavant sur mon ordinateur portable.

Cependant, je n'ai eu que 5% d'utilisation! Ma question est donc la suivante: comment puis-je adapter le programme à s’exécuter sur ma machine de jeu sous Windows 7 ou Linux en direct à une utilisation du processeur de 100%? Une autre chose qui serait intéressante mais non nécessaire est que le produit final puisse être un fichier .exe pouvant être facilement distribué et exécuté sur des ordinateurs Windows.

Merci beaucoup!

P.S. Bien sûr, ce programme ne fonctionnait pas vraiment avec les processeurs spécialisés de Crays 8, et c’est un tout autre problème… Si vous en savez quelque chose sur l’optimisation du code pour fonctionner sur les super-ordinateurs de Cray des années 90, faites-nous un cri!

76
bag-man

Si vous voulez 100% de CPU, vous devez utiliser plus d'un cœur. Pour ce faire, vous avez besoin de plusieurs threads.

Voici une version parallèle utilisant OpenMP:

J'ai dû augmenter la limite à 1000000 pour que cela prenne plus d'une seconde sur ma machine.

#include <stdio.h>
#include <time.h>
#include <omp.h>

int main() {
    double start, end;
    double runTime;
    start = omp_get_wtime();
    int num = 1,primes = 0;

    int limit = 1000000;

#pragma omp parallel for schedule(dynamic) reduction(+ : primes)
    for (num = 1; num <= limit; num++) { 
        int i = 2; 
        while(i <= num) { 
            if(num % i == 0)
                break;
            i++; 
        }
        if(i == num)
            primes++;
//      printf("%d prime numbers calculated\n",primes);
    }

    end = omp_get_wtime();
    runTime = end - start;
    printf("This machine calculated all %d prime numbers under %d in %g seconds\n",primes,limit,runTime);

    return 0;
}

Sortie:

Cette machine a calculé tous les 78498 nombres premiers inférieurs à 1000000 en 29.753 secondes.

Voici votre 100% CPU:

enter image description here

80
Mysticial

Vous exécutez un processus sur une machine multi-cœur - il ne s'exécute donc que sur un seul cœur.

La solution est assez simple, car vous essayez simplement de verrouiller le processeur - si vous avez N cœurs, exécutez votre programme N fois (en parallèle, bien sûr).

Exemple

Voici un code qui exécute votre programme NUM_OF_CORES fois en parallèle. C'est du code POSIXy - il utilise fork - donc vous devriez l'exécuter sous Linux. Si ce que je lis à propos de Cray est correct, il serait peut-être plus facile de porter ce code que le code OpenMP de l'autre réponse.

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define NUM_OF_CORES 8
#define MAX_PRIME 100000

void do_primes()
{
    unsigned long i, num, primes = 0;
    for (num = 1; num <= MAX_PRIME; ++num) {
        for (i = 2; (i <= num) && (num % i != 0); ++i);
        if (i == num)
            ++primes;
    }
    printf("Calculated %d primes.\n", primes);
}

int main(int argc, char ** argv)
{
    time_t start, end;
    time_t run_time;
    unsigned long i;
    pid_t pids[NUM_OF_CORES];

    /* start of test */
    start = time(NULL);
    for (i = 0; i < NUM_OF_CORES; ++i) {
        if (!(pids[i] = fork())) {
            do_primes();
            exit(0);
        }
        if (pids[i] < 0) {
            perror("Fork");
            exit(1);
        }
    }
    for (i = 0; i < NUM_OF_CORES; ++i) {
        waitpid(pids[i], NULL, 0);
    }
    end = time(NULL);
    run_time = (end - start);
    printf("This machine calculated all prime numbers under %d %d times "
           "in %d seconds\n", MAX_PRIME, NUM_OF_CORES, run_time);
    return 0;
}

Sortie

$ ./primes 
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
Calculated 9592 primes.
This machine calculated all prime numbers under 100000 8 times in 8 seconds
23
cha0site

nous voulons vraiment voir à quelle vitesse cela peut aller!

Votre algorithme pour générer des nombres premiers est très inefficace. Comparez-le à primegen qui génère les nombres premiers 50847534 jusqu'à 1000000000 en 8 secondes seulement sur un Pentium II-350.

Pour utiliser tous les processeurs facilement, vous pouvez résoudre un problème parallèle embarrassant par exemple, calculer ensemble de Mandelbrot ou utiliser programmation génétique pour Paint Mona Lisa dans plusieurs threads (processus).

Une autre approche consiste à utiliser un programme de référence existant pour le superordinateur Cray et à le transférer sur un PC moderne.

8
jfs

Si vous obtenez 15% sur un processeur à cœur hexagonal, c'est parce que votre code utilise 1 cœur à 100%. 100/6 = 16,67%, ce qui, avec une moyenne mobile avec planification du processus (votre processus fonctionnerait avec une priorité normale) pourrait facilement être rapporté à 15%. 

Par conséquent, pour utiliser 100% du processeur, vous devez utiliser tous les cœurs de votre processeur - lancez 6 chemins de code d'exécution parallèles pour un processeur à cœur hexagonal et disposez de cette échelle pour le nombre de processeurs de votre machine Cray :)

4
Carl

Sachez également que comment vous chargez le processeur. Un processeur peut effectuer un grand nombre de tâches différentes. Si beaucoup d'entre elles sont signalées comme "chargeant le processeur à 100%", elles peuvent utiliser chacune 100% des différentes parties du processeur. En d’autres termes, il est très difficile de comparer deux processeurs différents en termes de performances, et en particulier deux architectures de processeur différentes. L'exécution de la tâche A peut favoriser une unité centrale par rapport à une autre, alors que l'exécution de la tâche B peut être facilement l'inverse (car les deux unités centrales peuvent avoir des ressources différentes en interne et peuvent exécuter du code très différemment).

C’est la raison pour laquelle le logiciel est tout aussi important que le matériel pour optimiser la performance des ordinateurs. C’est également très vrai pour les "superordinateurs".

Une mesure des performances du processeur peut être une instruction par seconde, mais là encore, les instructions ne sont pas créées de la même manière sur différentes architectures de processeur. Une autre mesure pourrait être les performances du cache IO, mais l'infrastructure du cache n'est pas égale non plus. Ensuite, une mesure pourrait être le nombre d’instructions par watt utilisé, car la fourniture de puissance et la dissipation sont souvent un facteur limitant lors de la conception d’un ordinateur en cluster.

Votre première question devrait donc être: quel paramètre de performance est important pour vous? Que voulez-vous mesurer? Si vous voulez voir quelle machine obtient le plus de FPS de Quake 4, la réponse est simple. votre console de jeu le fera, car le Cray ne peut pas exécuter ce programme du tout ;-)

Salutations, Steen

2
Steen Schmidt

TLDR; La réponse acceptée est à la fois inefficace et incompatible. Suivre algo fonctionne 100x plus rapidement.

Le compilateur gcc disponible sur MAC ne peut pas exécuter omp. Je devais installer llvm (brew install llvm ). Mais je je n'ai pas vu le processeur inactif en panne lors de l'exécution de la version OMP. 

Voici une capture d’écran alors que la version OMP était en cours d’exécution .  enter image description here

Alternativement, j'ai utilisé le thread POSIX de base, qui peut être exécuté avec n'importe quel compilateur c et vu presque tout le processeur utilisé lorsque nos of thread = no of cores = 4 (MacBook Pro, Intel Core i5 à 2,3 GHz). Voici le programme -

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define NUM_THREADS     10
#define THREAD_LOAD 100000
using namespace std;

struct prime_range {
    int min;
    int max;
    int total;
};

void* findPrime(void *threadarg)
{
    int i, primes = 0;
    struct prime_range *this_range;
    this_range = (struct prime_range *) threadarg;

    int minLimit =  this_range -> min ;
    int maxLimit =  this_range -> max ;
    int flag = false;
    while (minLimit <= maxLimit) {
        i = 2;
        int lim = ceil(sqrt(minLimit));
        while (i <= lim) {
            if (minLimit % i == 0){
                flag = true;
                break;
            }
            i++;
        }
        if (!flag){
            primes++;
        }
        flag = false;
        minLimit++;
    }
    this_range ->total = primes;
    pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
    struct timespec start, finish;
    double elapsed;

    clock_gettime(CLOCK_MONOTONIC, &start);

    pthread_t threads[NUM_THREADS];
    struct prime_range pr[NUM_THREADS];
    int rc;
    pthread_attr_t attr;
    void *status;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    for(int t=1; t<= NUM_THREADS; t++){
        pr[t].min = (t-1) * THREAD_LOAD + 1;
        pr[t].max = t*THREAD_LOAD;
        rc = pthread_create(&threads[t], NULL, findPrime,(void *)&pr[t]);
        if (rc){
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }
    int totalPrimesFound = 0;
    // free attribute and wait for the other threads
    pthread_attr_destroy(&attr);
    for(int t=1; t<= NUM_THREADS; t++){
        rc = pthread_join(threads[t], &status);
        if (rc) {
            printf("Error:unable to join, %d" ,rc);
            exit(-1);
        }
        totalPrimesFound += pr[t].total;
    }
    clock_gettime(CLOCK_MONOTONIC, &finish);
    elapsed = (finish.tv_sec - start.tv_sec);
    elapsed += (finish.tv_nsec - start.tv_nsec) / 1000000000.0;
    printf("This machine calculated all %d prime numbers under %d in %lf seconds\n",totalPrimesFound, NUM_THREADS*THREAD_LOAD, elapsed);
    pthread_exit(NULL);
}

Remarquez comment tout le processeur est utilisé jusqu'à -  enter image description here

P.S. - Si vous augmentez le nombre de threads, l'utilisation réelle du processeur diminue (essayez de créer un nombre de threads = 20), car le système utilise plus de temps pour la commutation de contexte que pour l'informatique réelle.

En passant, ma machine n’est pas aussi costaud que @mystical (réponse acceptée). Mais ma version avec le threading POSIX de base fonctionne bien plus rapidement que celle d’OMP. Voici le résultat -

 enter image description here

P.S. Augmentez la charge de threads à 2,5 millions pour voir l'utilisation du processeur, car elle se termine en moins d'une seconde.

0
sapy

Essayez de paralléliser votre programme en utilisant, par exemple, OpenMP. C'est un cadre très simple et efficace pour la création de programmes parallèles.

0
mikithskegg

Essayez simplement de compresser et décompresser un fichier volumineux. Aucune opération lourde d’entrée/sortie ne peut utiliser cpu.

0
Nima Mohammadi

Pour améliorer rapidement un noyau, supprimez les appels système pour réduire le changement de contexte. Supprimer ces lignes:

system("clear");
printf("%d prime numbers calculated\n",primes);

Le premier est particulièrement grave, car il engendrera un nouveau processus à chaque itération.

0
Joel