Ich habe versucht, den CPU-Kern mit Ruby voll auszunutzen

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?

Nutzen Sie den CPU-Kern in Ruby voll aus

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.

Mit Multithread verarbeiten

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.

Schreiben Sie mit der C-Erweiterungsbibliothek

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.

Machen Sie es Multithread

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.

Schreiben Sie eine C-Erweiterungsbibliothek für die GVL-Version

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

GVL Release!

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.

Das Ende

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

Ich habe versucht, den CPU-Kern mit Ruby voll auszunutzen
Ich habe versucht, ein übergeordnetes Wertklasseobjekt in Ruby zu erstellen
Ich habe die grundlegende Grammatik von Ruby kurz zusammengefasst
Ich habe einen RESAS-API-Client in Java erstellt
Ich habe versucht, das Problem der "mehrstufigen Auswahl" mit Ruby zu lösen
Ich habe versucht, einen Numeron zu erstellen, der mit Ruby nicht gut ist
Ich möchte den Wert von Attribute in Selenium of Ruby ändern
Ich habe versucht, die Sitzung in Rails zu organisieren
Ich habe versucht, das Problem der Tribonacci-Sequenz in Ruby mit Wiederholung zu lösen.
[Ruby] Ich habe versucht, die häufigen Methoden in Paiza zusammenzufassen
[Ruby] Ich habe versucht, die häufigen Methoden mit paiza ② zusammenzufassen
Ich möchte den Wert in Ruby erhalten
Ich habe versucht, ein Beispielprogramm mit dem Problem des Datenbankspezialisten für domänengesteuertes Design zu erstellen
Ich habe versucht, das Problem der Tribonacci-Sequenz in Ruby zu lösen (Zeitlimit 10 Minuten).
05. Ich habe versucht, die Quelle von Spring Boot zu löschen
Ich habe versucht, eine Anmeldefunktion mit Java zu erstellen
Ich habe versucht, die Methode der gegenseitigen Teilung von Eugrid in Java zu implementieren
Da der Befehl du, der bei voller Kapazität verwendet wird, schwierig zu verwenden ist, habe ich versucht, ihn mit Rubin zu umwickeln
Ich habe versucht, innerhalb von 3 Monaten einen Antrag von unerfahren zu stellen
Ich habe versucht, die Grundlagen von Kotlin und Java zusammenzufassen
Ich habe versucht, eine Umgebung mit WSL2 + Docker + VSCode zu erstellen
[Ruby] Ich möchte die Reihenfolge der Hash-Tabelle umkehren
Ich habe mir die Rosen von Versailles angesehen und versucht, das Schlusslied in Java zu reproduzieren
Ich habe versucht, das Problem mit der Ruby-Karaoke-Maschine zu lösen (es gibt ein Beispiel für die Antwort).
Ich habe versucht, die Methode zu erklären
Ich habe versucht, das Problem mit dem Ruby-Bonusgetränk zu lösen (es gibt ein Beispiel für die Antwort).
Ich habe versucht, die Cache-Funktion von Application Container Cloud Service in der lokalen Umgebung zu entwickeln
Ich habe versucht, den Weihnachtsbaum in einem Lebensspiel zu beleuchten
Daten sortieren Absteigend, aufsteigend / Schienen
Ich habe versucht, mit Docker eine Plant UML Server-Umgebung zu erstellen
Ich habe versucht, Code wie eine Typdeklaration in Ruby zu schreiben
[Rubiy] Heute Abend habe ich versucht, die Schleifenverarbeitung zusammenzufassen [Zeiten, Pause ...]
Ich habe versucht, Java-Anfänger so einzustellen, dass sie Tastenkombinationen in Eclipse verwenden
Ich habe versucht, den Betrieb des gRPC-Servers mit grpcurl zu überprüfen
Ich habe versucht, die Methoden von Java String und StringBuilder zusammenzufassen
Ich habe versucht, das Problem des Google Tech Dev Guide zu lösen
Ich habe versucht, mir zu erlauben, die Verzögerung für den Android UDP-Client einzustellen
Ich habe versucht, das Problem bei der Erstellung von Ruby-Bingokarten zu lösen (es gibt ein Beispiel für die Antwort).
Nachdem ich Progate gelernt hatte, versuchte ich, eine SNS-Anwendung mit Rails in der lokalen Umgebung zu erstellen
Ich habe ein Kalenderproblem mit Ruby versucht
Ich habe versucht, die verwendeten Methoden zusammenzufassen
Ich habe das neue Yuan-Problem in Java ausprobiert
Ich habe versucht, das Iterator-Muster zu implementieren
Ich habe versucht, die Stream-API zusammenzufassen
Ich habe die AutoValue-Bibliothek mit Intellij ausprobiert
Ich möchte @Autowired in Servlet verwenden
Ich habe versucht, Selen wie JQuery zu verwenden
[Ruby] Ich möchte nur das ungerade Zeichen in der Zeichenfolge ausgeben
Schritte zum Ausführen von Spring Boot beziehen sich auf die Werte in der Eigenschaftendatei