Ich bin seit 2008 Programmierer für Lebensmittelprotokolle und immer noch Programmierer Oishii Tsukasa. Der Grund, warum der Name Hiragana lautet, bestand darin, die Mitarbeiter des Unternehmens daran zu hindern, ihre Aktivitäten preiszugeben, wenn es selten vorkam, im Internet aktiv zu sein. Ich habe eine schwarze Geschichte, die 1995 eine Homepage namens "Tsukasa's Room" hatte. Es ist nicht großartig? Wäre es nicht super schlimm, wenn jemand in der Firma es herausfinden würde? : cat2: Ich habe versucht, ein Katzenpiktogramm ohne Bedeutung zu erstellen. Ich habe versucht, ein wenig zu spielen. Vielleicht werde ich es nicht mehr lesen?
Selbst wenn die Thread-Programmierung in Ruby erfolgt, kann der Kern der CPU nicht verwendet werden. Dies liegt daran, dass in Ruby aufgrund der Giant VM-Sperre (GVL) immer nur ein Thread gleichzeitig ausgeführt wird. Beim Warten auf E / A wird GVL freigegeben, sodass mehrere Threads gleichzeitig ausgeführt werden können. Es funktioniert effektiv, wenn Sie mehrere URLs zum Abrufen von Daten drücken.
Dies ist bei numerischen Berechnungen nicht der Fall. Angenommen, Sie möchten das Produkt aus zwei 512 x 512-Matrizen berechnen.
#Machen Sie zwei 512x512 Matrizen
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)
Berechnen Sie das Produkt dieser beiden Matrizen "a" und "b". Um ehrlich zu sein, beträgt der Berechnungsbetrag $ O (n ^ {3}) $, da dreifache Schleifen erforderlich sind.
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
Ich werde die Bearbeitungszeit messen.
require 'benchmark'
puts Benchmark.realtime { m_and(a, b, num) }
18.133936062455177
Es dauerte ungefähr 18 Sekunden. Die Maschine, die ich betrieben habe, hat vier CPU-Kerne. Werfen wir einen Blick auf die CPU-Auslastung während der Verarbeitung.
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
Nur ein Kern gibt sein Bestes.
Ich werde versuchen, mit Multithread zu verarbeiten. Da die Berechnung einer Zeile x Spalte unabhängig voneinander parallel durchgeführt werden kann, teilen wir die Daten in dieser Einheit auf und lassen sie von jedem Thread verarbeitet werden.
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
Da es 4 CPU-Kerne gibt, wird es mit 4 Threads ausgeführt.
puts Benchmark.realtime { t_m_and(a, b, num, 4) }
18.22166531533003
Dies dauerte ebenfalls etwa 18 Sekunden. Die CPU-Auslastung während der Ausführung war wie folgt.
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
Ich verwende alle Kerne richtig, aber da sich nur ein Thread gleichzeitig bewegen kann, ist die Gesamtzeit dieselbe wie bei der sequentiellen Verarbeitung. Ich werde es schaffen, alle Kerne voll auszunutzen.
Wenn Sie GVL freigeben, können Sie mehrere Threads gleichzeitig ausführen. Bei dieser Matrixberechnung sollte sich die Verarbeitung jedes Threads nicht gegenseitig beeinflussen, sodass zu erwarten ist, dass es auch dann kein Problem gibt, wenn GVL freigegeben wird. Um GVL freizugeben, muss eine Erweiterungsbibliothek in C geschrieben werden. Schreiben Sie daher zuerst die Verarbeitung des Matrizenprodukts in C-Sprache.
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);
}
Das zweidimensionale Array wird zur Vereinfachung der Verwendung in ein eindimensionales Array konvertiert. ary_to_cary
konvertiert das Array von ruby's in c's long *
(keine Standardfunktion, die dafür geschrieben wurde, obwohl sie nicht aufgeführt ist).
Die Funktion and_matrix
lautet wie folgt.
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;
}
Lass uns das machen.
fm = FullcoreMatrix.new
puts Benchmark.realtime { fm.m_and(a, b, num) }
0.39124061167240143
Es ist überwältigend schnell! Der Vorgang, der 18 Sekunden dauerte, beträgt jetzt 391 Millisekunden.
Die CPU-Auslastung ist wie folgt und nur ein Kern arbeitet hart.
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
Schreiben Sie ihn basierend auf diesem Code in die Multithread-Verarbeitung um.
Da es problematisch ist, werde ich den Prozess schnell mit OpenMP schreiben.
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;
}
Wenn ich es laufen lasse,
0.13279788196086884
Nun, es ist schneller, obwohl ich GVL nicht veröffentlicht habe. Das liegt daran, dass es nicht durch Rubys Thread geht. Wenn man bedenkt, dass der Wert nahe bei einem Viertel liegt, scheinen die vier Kerne effektiv genutzt zu werden.
Es ist zu schnell, um mit sar
richtig zu messen. Berechnen wir also mit einer 2048x2048-Matrix.
10.213115755468607
Es dauert wirklich 10 Sekunden. Die CPU-Auslastung ist wie folgt.
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
Wir nutzen jetzt alle Kerne voll aus.
Es scheint, als würden Sie nicht alle Kerne in Ruby voll ausnutzen. Sie haben gerade die Thread-Programmierung in C geschrieben, aber träumen Sie davon? Ich habe das Gefühl, irgendwie verloren zu haben, also werde ich versuchen, alle Kerne nach der Verwendung von Thread of Ruby voll auszunutzen. Ich werde GVL wie ursprünglich geplant veröffentlichen.
Um GVL freizugeben, müssen Sie es in die C-Erweiterungsbibliothek schreiben. Außerdem sollten Sie beim Freigeben von GVL keine Ruby-Funktionen berühren. Aus diesem Grund bieten wir eine Methode an, mit der zwei Matrizen in einer Instanz der C-Erweiterungsbibliothek in "long *" konvertiert und eine Berechnung für eine Spalte x eine Zeile durchgeführt werden können. Dies ist ein Bild, das diese Methode im Thread-Prozess von Ruby aufruft.
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);
}
Ich habe die Funktion "destroy_matix" weggelassen (nenne sie einfach "ruby_xfree").
Die Funktion and_matrix
lautet wie folgt.
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;
}
Der Grund, warum and_matrix
ein Argument mit void *
empfängt und einen Wert mitvoid *
zurückgibt, besteht darin, das spätere Einfügen des GVL-Freigabeprozesses zu vereinfachen.
Diese Bibliothek wird in in Ruby geschriebenem Thread-Verarbeitungscode verwendet.
require './nonblock_matrix'
def t_m_and2(a, b, num, t_size)
m = []
queue = Queue.new
threads = []
nb = NonblockMatrix.new(a, b, num) #Verwenden Sie die C-Erweiterungsbibliothek
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) #Berechnungsverarbeitung einer Spalte x einer Zeile, die in der C-Erweiterungsbibliothek implementiert ist
rescue ThreadError
raise unless q.empty?
end
end
end
end
threads.each(&:join)
m
end
Ich werde es versuchen.
0.48769768700003624
Es sind 488 Millisekunden! Selbst wenn Sie den Prozess hier auf C ändern, ist er erheblich schneller geworden. Es ist so schnell wie das Schreiben der gesamten Verarbeitung in C. Natürlich werden die Threads einzeln ausgeführt, sodass die CPU-Auslastung wie folgt ist.
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
Lassen Sie zum Schluss GVL los.
Rufen Sie die in der C-Erweiterungsbibliothek implementierte Funktion and_matrix
auf, nachdem Sie GVL freigegeben haben.
Verwenden Sie die Funktion rb_thread_call_without_gvl
, um die GVL freizugeben und die Funktion aufzurufen.
rb_thread_call_without_gvl(and_matrix, &data, RUBY_UBF_PROCESS, NULL);
Die von der Funktion rb_thread_call_without_gvl
angegebene Funktion muss ein Argument mit void *
empfangen und einen Wert mitvoid *
zurückgeben. Es ist einfach, weil and_matrix
die Schnittstelle dafür definiert hat.
Ich werde es versuchen.
1.4591152295470238
Das ist spät.
Schauen wir uns die CPU-Auslastung an.
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
Wir haben den CPU-Kern nicht voll ausgenutzt, konnten aber 60% überschreiten. Es scheint jedoch, dass es viele nutzlose Dinge tut, weil es langsam ist, obwohl es CPU-Ressourcen verwendet. Der Wert von "System" springt hoch.
Es gab einen Kommentar wie unten in thread.c
von 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.
Es scheint, dass die Kosten höher sind, wenn Sie eine Funktion aufrufen, die wie diese Zeit viele Male sofort endet.
Die Berechnung der 2048x2048-Matrix mit dem ersten Ruby-Code dauerte übrigens 1467 Sekunden. : cat2:
Der Artikel von morgen lautet @ maguhiros "Ich habe einen Bitrise-Schritt zum Senden von Nachrichten an MS-Teams gemacht".
Recommended Posts