J'ai essayé d'utiliser pleinement le cœur du processeur avec Ruby

Je suis programmeur pour les journaux alimentaires depuis 2008, et je suis toujours programmeur Oishii Tsukasa. La raison pour laquelle le nom est Hiragana était d'empêcher les gens de l'entreprise de révéler leurs activités alors qu'il était rare d'être actif sur Internet. J'ai une histoire noire qui avait une page d'accueil appelée "Tsukasa's Room" en 1995. Ce n'est pas génial? Ne serait-ce pas super si quelqu'un dans l'entreprise le découvrait? : cat2: J'ai essayé de mettre un pictogramme de chat sans aucune signification. J'ai essayé de jouer un peu. Peut-être que je ne vais plus le lire?

Exploitez pleinement le cœur du processeur dans Ruby

Même si la programmation des threads est effectuée dans Ruby, le cœur du processeur ne peut pas être utilisé. Cela est dû au fait que Ruby n'a toujours qu'un seul thread en cours d'exécution en même temps en raison du verrou Giant VM (GVL). Lors de l'attente d'E / S, GVL est libéré afin que plusieurs threads puissent s'exécuter en même temps. Cela fonctionnera efficacement lorsque vous cliquez sur plusieurs URL pour récupérer des données.

Ce n'est pas le cas lors de l'exécution de calculs numériques. Par exemple, disons que vous souhaitez calculer le produit de deux matrices 512x512.

#Créer deux matrices 512x512
def build(num, max)
  m = []
  
  num.times do |i|
    m[i] = []
    num.times do |j|
      m[i][j] = rand(max)
    end
  end
  
  m
end

num = 512
max = 256

a = build(num, max)
b = build(num, max)

Calculez le produit de ces deux matrices, «a» et «b». Pour être honnête, le montant du calcul est $ O (n ^ {3}) $ car il nécessite des boucles triples.

def m_and(a, b, num)
  m = []
  
  num.times do |i|
    m[i] = []
    num.times do |j|
      m[i][j] = 0

      num.times do |k|
        m[i][j] += a[i][k] * b[k][j]
      end
    end
  end
  
  m
end

Je vais mesurer le temps de traitement.

require 'benchmark'
puts Benchmark.realtime { m_and(a, b, num) }
18.133936062455177

Cela a pris environ 18 secondes. La machine que j'ai exécutée a quatre cœurs de processeur. Jetons un coup d'œil à l'utilisation du processeur pendant le traitement.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     25.00      0.00      0.25      0.00      0.00     74.75
  0      0.00      0.00      0.00      0.00      0.00    100.00
  1      0.00      0.00      1.00      0.00      0.00     99.00
  2    100.00      0.00      0.00      0.00      0.00      0.00
  3      0.00      0.00      0.00      0.00      0.00    100.00

Un seul noyau fait de son mieux.

Processus avec multi-thread

Je vais essayer de traiter avec multi-thread. Puisque le calcul d'une ligne x colonne peut être effectué indépendamment en parallèle, divisons les données dans cette unité et laissons chaque thread le traiter.

def t_m_and(a, b, num, t_size)
  m = []
  queue = Queue.new
  threads = []

  num.times do |i|
    m[i] = []
    num.times do |j|
      m[i][j] = 0
      queue.push([i, j])
    end
  end

  t_size.times do
    threads << Thread.new(queue) do |q|
      until q.empty?
        begin
          ti, tj = q.pop(true)
          num.times do |k|
            m[ti][tj] += a[ti][k] * b[k][tj]
          end
        rescue ThreadError
          raise unless q.empty?
        end
      end
    end
  end
  threads.each(&:join)

  m
end

Puisqu'il y a 4 cœurs de CPU, il est exécuté avec 4 threads.

puts Benchmark.realtime { t_m_and(a, b, num, 4) }
18.22166531533003

Cela a également pris environ 18 secondes. L'utilisation du processeur pendant l'exécution était la suivante.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     25.06      0.00      0.00      0.00      0.00     74.94
  0     29.70      0.00      0.99      0.00      0.00     69.31
  1     28.00      0.00      0.00      0.00      0.00     72.00
  2     22.00      0.00      0.00      0.00      0.00     78.00
  3     20.20      0.00      0.00      0.00      0.00     79.80

J'utilise correctement tous les cœurs, mais comme un seul thread peut se déplacer en même temps, le temps total est le même que lors du traitement séquentiel. J'arriverai à utiliser pleinement tous les cœurs.

Écrire avec la bibliothèque d'extension C

Si vous libérez GVL, vous pouvez exécuter plusieurs threads en même temps. Puisque le traitement de chaque thread ne devrait pas affecter le calcul de la matrice cette fois, on peut s'attendre à ce qu'il n'y ait pas de problème même si GVL est libéré. Afin de libérer GVL, il est nécessaire d'écrire une bibliothèque d'extensions en C, donc d'abord écrire le traitement du produit des matrices en langage C.

static VALUE
and(VALUE self, VALUE a, VALUE b, VALUE num)
{
  long *ma, *mb, *mc;
  long n;
  VALUE result;

  n = NUM2LONG(num);
  ma = ary_to_cary(a, n);
  mb = ary_to_cary(b, n);

  mc = and_matrix(ma, mb, n);
  result = cary_to_ary(mc, n);

  ruby_xfree(ma);
  ruby_xfree(mb);
  ruby_xfree(mc);

  return result;
}

void
Init_fullcore_matrix(void)
{
  VALUE cFullcoreMatrix = rb_define_class("FullcoreMatrix", rb_cObject);

  rb_define_method(cFullcoreMatrix, "m_and", and, 3);
}

Le tableau bidimensionnel est converti en tableau unidimensionnel pour faciliter son utilisation. ʻAry_to_caryconvertit le tableau de ruby enlong * de c (pas une fonction standard, écrite pour cela, mais non répertoriée). La fonction ʻand_matrix est la suivante.

static long *
and_matrix(long *a, long *b, long num)
{
  long *m;
  long i, j, k;
  long index;

  m = (long*)ruby_xmalloc(sizeof(long) * num * num);

  for(i = 0; i < num; i++) {
    for(j = 0; j < num; j++) {
      index = i * num + j;
      m[index] = 0;
      for(k = 0; k < num; k++) {
        m[index] += a[i * num + k] * b[k * num + j];
      }
    }
  }

  return m;
}

Faisons cela.

fm = FullcoreMatrix.new 
puts Benchmark.realtime { fm.m_and(a, b, num) }
0.39124061167240143

C'est extrêmement rapide! Le processus qui a pris 18 secondes est maintenant de 391 millisecondes.

L'utilisation du processeur est la suivante et un seul cœur travaille dur.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     25.00      0.00      0.25      0.00      0.00     74.75
  0      0.00      0.00      1.00      0.00      0.00     99.00
  1      0.00      0.00      0.00      0.00      0.00    100.00
  2    100.00      0.00      0.00      0.00      0.00      0.00
  3      0.00      0.00      0.00      0.00      0.00    100.00

Sur la base de ce code, réécrivez-le en traitement multi-thread.

Faites-le multi-thread

Comme c'est gênant, j'écrirai le processus rapidement avec OpenMP.

static long *
and_matrix(long *a, long *b, long num)
{
  long *m;
  long i, j, k;
  long index;

  m = (long*)ruby_xmalloc(sizeof(long) * num * num);

  #pragma omp parallel for private(j, k, index)
  for(i = 0; i < num; i++) {
    for(j = 0; j < num; j++) {
      index = i * num + j;
      m[index] = 0;
      for(k = 0; k < num; k++) {
        m[index] += a[i * num + k] * b[k * num + j];
      }
    }
  }

  return m;
}

Quand je l'exécute,

0.13279788196086884

Eh bien, c'est plus rapide même si je n'ai pas publié GVL. C'est parce qu'il ne passe pas par Ruby's Thread. Considérant que la valeur est proche d'un quart, il semble que les quatre cœurs soient utilisés efficacement. C'est trop rapide pour mesurer correctement avec sar, alors calculons avec une matrice de 2048x2048.

10.213115755468607

Cela prend vraiment 10 secondes. L'utilisation du processeur est la suivante.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     99.50      0.00      0.50      0.00      0.00      0.00
  0    100.00      0.00      0.00      0.00      0.00      0.00
  1    100.00      0.00      0.00      0.00      0.00      0.00
  2    100.00      0.00      0.00      0.00      0.00      0.00
  3    100.00      0.00      0.00      0.00      0.00      0.00

Nous utilisons désormais pleinement tous les cœurs.

Il semble que vous n'utilisiez pas pleinement tous les cœurs de Ruby, vous venez d'écrire la programmation de threads en C, mais en rêvez-vous? J'ai l'impression d'avoir perdu d'une manière ou d'une autre, alors j'essaierai d'utiliser pleinement tous les cœurs après avoir utilisé Thread of Ruby. Je publierai GVL comme initialement prévu.

Ecrire une bibliothèque d'extensions C pour la version GVL

Pour publier GVL, vous devez l'écrire dans la bibliothèque d'extension C, mais en plus, vous ne devez pas toucher aux fonctions Ruby lors de la publication de GVL. Pour cette raison, nous fournissons une méthode pour convertir deux matrices en «long *» dans une instance de la bibliothèque d'extensions C et effectuer un calcul d'une colonne x une ligne. Ceci est une image qui appelle cette méthode dans le processus Thread de Ruby.

typedef struct _matrix {
  long *a;
  long *b;
  long num;
} *matrix;

struct and_data {
  matrix m;
  long i;
  long j;
  long result;
};

...

static VALUE
and(VALUE self, VALUE i, VALUE j)
{
  matrix m;
  struct and_data data;

  Data_Get_Struct(self, struct _matrix, m);
  data.m = m;
  data.i = NUM2LONG(i);
  data.j = NUM2LONG(j);

  and_matrix(&data);

  return LONG2NUM(data.result);
}

static VALUE
new(VALUE klass, VALUE a, VALUE b, VALUE num)
{
  matrix m;
  VALUE obj;

  obj = Data_Make_Struct(klass, struct _matrix, NULL, destroy_matix, m);

  m->num = NUM2LONG(num);
  m->a = ary_to_cary(a, m->num);
  m->b = ary_to_cary(b, m->num);

  return obj;
}

void
Init_nonblock_matrix(void)
{
  VALUE cNBMatrix = rb_define_class("NonblockMatrix", rb_cObject);

  rb_define_singleton_method(cNBMatrix, "new", new, 3);
  rb_define_method(cNBMatrix, "m_and", and, 2);
}

J'ai omis la fonction destroy_matix (appelez-la simplement ruby_xfree).

La fonction ʻand_matrix` est la suivante.

static void *
and_matrix(void *data)
{
  long i, j, num, k, result;
  long *a, *b;
  struct and_data *d;

  d = (struct and_data*)data;

  num = d->m->num;
  a = d->m->a;
  b = d->m->b;
  i = d->i;
  j = d->j;

  result = 0;
  for(k = 0; k < num; k++) {
    result += a[i * num + k] * b[k * num + j];
  }

  d->result = result;

  return NULL;
}

La raison pour laquelle ʻand_matrix reçoit un argument avec void * ʻet renvoie une valeur avec` void * ʻest pour faciliter l'insertion ultérieure du processus de publication de GVL.

Cette bibliothèque est utilisée dans le code de traitement des threads écrit en Ruby.

require './nonblock_matrix'
def t_m_and2(a, b, num, t_size)
  m = []
  queue = Queue.new
  threads = []
  nb = NonblockMatrix.new(a, b, num) #Utiliser la bibliothèque d'extensions C

  num.times do |i|
    m[i] = []
    num.times do |j|
      queue.push([i, j])
    end
  end

  t_size.times do
    threads << Thread.new(queue) do |q|
      until q.empty?
        begin
          ti, tj = q.pop(true)
          m[ti][tj] = nb.m_and(ti, tj) #Traitement du calcul d'une colonne x une ligne implémenté dans la bibliothèque d'extensions C
        rescue ThreadError
          raise unless q.empty?
        end
      end
    end
  end
  threads.each(&:join)

  m
end

Je vais essayer.

0.48769768700003624

C'est 488 millisecondes! Même le simple fait de changer le processus ici en C l'a rendu considérablement plus rapide. C'est aussi rapide que d'écrire tout le traitement en C. Naturellement, les threads s'exécutent un par un, donc l'utilisation du processeur est la suivante.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     25.00      0.00      0.25      0.00      0.00     74.75
  0     26.00      0.00      1.00      0.00      0.00     73.00
  1     20.00      0.00      0.00      0.00      0.00     80.00
  2     30.00      0.00      0.00      0.00      0.00     70.00
  3     23.23      0.00      0.00      0.00      0.00     76.77

Sortie GVL!

Enfin, libérez GVL. Appelez la fonction ʻand_matriximplémentée dans la bibliothèque d'extension C après avoir publié GVL. Pour libérer la GVL et appeler la fonction, utilisez la fonctionrb_thread_call_without_gvl`.

rb_thread_call_without_gvl(and_matrix, &data, RUBY_UBF_PROCESS, NULL);

La fonction spécifiée par la fonction rb_thread_call_without_gvl doit recevoir un argument avec void * ʻet renvoyer une valeur avec void *. C'est facile car ʻand_matrix a défini l'interface pour cela.

Je vais essayer.

1.4591152295470238

C'est tard.

Regardons l'utilisation du processeur.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     56.11      0.00     10.28      0.00      0.00     33.61
  0     60.44      0.00     14.29      0.00      0.00     25.27
  1     61.80      0.00      8.99      0.00      0.00     29.21
  2     42.86      0.00      9.89      0.00      0.00     47.25
  3     58.43      0.00      7.87      0.00      0.00     33.71

Nous n'avons pas pleinement utilisé le cœur du processeur, mais nous avons pu dépasser les 60%. Cependant, il semble qu'il fasse beaucoup de choses inutiles car il est lent même s'il utilise des ressources CPU. La valeur de «system» augmente.

Il y avait un commentaire comme ci-dessous dans thread.c de ruby.

 NOTE: Releasing GVL and re-acquiring GVL may be expensive operations
       for a short running `func()'. Be sure to benchmark and use this
       mechanism when `func()' consumes enough time.

Il semble que le coût sera plus élevé si vous appelez plusieurs fois une fonction qui se termine immédiatement comme cette fois.

La fin

À propos, il a fallu 1467 secondes pour calculer la matrice 2048x2048 avec le premier code rubis. : cat2:

L'article de demain est celui de @ maguhiro "J'ai fait une étape Bitrise pour envoyer des messages aux équipes MS".

Recommended Posts

J'ai essayé d'utiliser pleinement le cœur du processeur avec Ruby
J'ai essayé de créer une classe parent d'objet de valeur dans Ruby
J'ai brièvement résumé la grammaire de base de Ruby
J'ai créé un client RESAS-API en Java
J'ai essayé de résoudre le problème de la "sélection multi-étapes" avec Ruby
J'ai essayé de faire un Numeron qui n'est pas bon avec Ruby
Je veux changer la valeur de l'attribut dans Selenium of Ruby
J'ai essayé d'organiser la session en Rails
J'ai essayé de résoudre le problème de la séquence Tribonacci en Ruby, avec récurrence.
[Ruby] J'ai essayé de résumer les méthodes fréquentes dans paiza
[Ruby] J'ai essayé de résumer les méthodes fréquentes avec paiza ②
Je veux obtenir la valeur en Ruby
J'ai essayé de créer un exemple de programme en utilisant le problème du spécialiste des bases de données dans la conception pilotée par domaine
J'ai essayé de résoudre le problème de la séquence Tribonacci en Ruby (temps limite 10 minutes)
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de créer une fonction de connexion avec Java
J'ai essayé d'implémenter la méthode de division mutuelle d'Eugrid en Java
Comme la commande du utilisée lorsque la capacité est pleine est difficile à utiliser, j'ai essayé de l'envelopper avec du rubis
J'ai essayé de faire une demande en 3 mois d'inexpérimenté
J'ai essayé de résumer les bases de kotlin et java
J'ai essayé de créer un environnement de WSL2 + Docker + VSCode
[Ruby] Je souhaite inverser l'ordre de la table de hachage
J'ai fini de regarder les roses de Versailles, alors j'ai essayé de reproduire la chanson de fin en Java
J'ai essayé de résoudre le problème de la machine à karaoké Ruby (il y a un exemple de réponse)
J'ai essayé d'expliquer la méthode
J'ai essayé de résoudre le problème de la boisson bonus Ruby (il y a un exemple de réponse)
J'ai essayé de développer la fonction de cache d'Application Container Cloud Service dans l'environnement local
J'ai essayé d'illuminer le sapin de Noël dans un jeu de la vie
Tri des données Décroissant, croissant / Rails
J'ai essayé de créer un environnement de serveur UML Plant avec Docker
J'ai essayé d'écrire du code comme une déclaration de type en Ruby
[Rubiy] J'ai essayé de résumer le traitement de la boucle ce soir [fois, pause ...]
J'ai essayé de configurer les débutants Java pour qu'ils utilisent des touches de raccourci dans eclipse
J'ai essayé de vérifier le fonctionnement du serveur gRPC avec grpcurl
J'ai essayé de résumer les méthodes de Java String et StringBuilder
J'ai essayé de résoudre le problème de Google Tech Dev Guide
J'ai essayé de me permettre de définir le délai pour le client Android UDP
J'ai essayé de résoudre le problème de création de carte de bingo Ruby (il y a un exemple de réponse)
Après avoir appris Progate, j'ai essayé de créer une application SNS en utilisant Rails dans l'environnement local
J'ai essayé un problème de calendrier avec Ruby
J'ai essayé de résumer les méthodes utilisées
J'ai essayé le nouveau yuan à Java
J'ai essayé d'implémenter le modèle Iterator
J'ai essayé de résumer l'API Stream
J'ai essayé la bibliothèque AutoValue avec Intellij
Je veux utiliser @Autowired dans Servlet
J'ai essayé d'utiliser Selenium comme JQuery
[Ruby] Je souhaite afficher uniquement le caractère impair dans la chaîne de caractères
Étapes pour rendre Spring Boot capable de faire référence à la valeur dans le fichier de propriétés