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?
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.
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.
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 en
long * 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.
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.
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
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 fonction
rb_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.
À 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