Ruby-Programmierung: Erweiterung Ruby mit C

Aus Wikibooks

Zurück zum Inhaltsverzeichnis.

Ruby erlaubt es bei Bedarf nativen Code zu verwenden und ihn aus Rubyquelltexten heraus anzusprechen. Nativer Code beschreibt dabei Programmteile, die in einer anderen Programmiersprache geschrieben wurde und für die jeweilige Plattform übersetzt wurden. Das Ergebnis ist je nach Plattform eine .dll oder .so oder Ähnliches. Das ist insbesondere von Vorteil, wenn man auf Geschwindigkeitsprobleme stößt oder bereits vorhandene Bibliotheken benutzen möchte.

In diesem Kapitel soll eine C-Bibliothek entwickelt werden, die in einem Rubyprogramm eingebettet wird, um die Geschwindigkeit des Programms zu erhöhen. Dazu vergleichen wir mehrere Möglichkeiten auf ihre Vor- und Nachteile insbesondere in Hinblick auf Geschwindigkeit beim Ausführen des Programms und Einfachheit der Implementation. Ein Wrapper, der es erlaubt bereits vorhandene C-Bibliotheken anzusprechen würde dann ähnliche Schritte benötigen.

Beispielprogramm[Bearbeiten]

Das Programm berechnet die Kreiszahl Pi und der entsprechende Quelltext in reinem Ruby sieht so aus:

class CalcPi
  def withPlainRuby(n)
    count = 0
    n.times do
      x = rand
      y = rand
	
      if x**2 + y**2 < 1
  	    count += 1;
      end
    end	
    return 4.0*count/n
   end
end

n = ARGV[0].to_i
startTime = Time.new
CalcPi.new.withPlainRuby(n)
puts "Processing withplainRuby took #{ Time.new - startTime } seconds."

Der Algorithmus folgt dem Beispiel unter w:Kreiszahl#Statistische Bestimmung und ist für eine tatsächliche Umsetzung nicht zu Empfehlen, da er nicht deterministisch ist. Bessere Berechnungsmöglichkeiten sind Reihendarstellungen, da diese eine definiertes Verhalten haben, wie sich die Genauigkeit der Berechnung in Abhängigkeit der Iterationen verhält. Die statistische Bestimmung eignet sich jedoch gut, um die Geschwindigkeit in verschiedenen Implementationen zu vergleichen, da sie leicht zu verstehen und einfach zu implementieren ist. Die Zeit die das Programm benötigt unterscheidet sich je nach System und Anzahl der Iterationen und Bedarf ein paar Versuche, um eine sichtbare Dauer zu benötigen. In diesen Beispiel erzeugte das Programm folgende Ausgabe: Processing withplainRuby took 3.682503619 seconds.

RubyInline[Bearbeiten]

Die beiden kommenden Abschnitte erfordern das kompillieren von C-Code und die Integration in Ruby. Dafür benötigt man die C-Bibliothek ruby.h und einen Compiler. Ke nach System unterscheidet sich die Installation der benötigten Software, falls Sie ein Linux verwenden, benutzen Sie am bessten die Paketverwaltung des Systems. Als Compiler eignet sich die gcc, die Headerdatei befindet sich in dem Ruby-Entwicklerpaket. Unter MacOSX müssen Sie für beide Probleme X-Code installieren.

Das Gem RubyInline erlaubt es einem C-Code direkt in den Rubycode einzubetten und damit seine Klasse zu erweitern.

require 'rubygems'
require 'inline'

class CalcPi
  inline :C do |builder|
  builder.flags << 'std=c99'
  builder.c '
    souble withInlineC(int n) {
      int count = 0;
      for(int i = 0; i <= n; i++) {
      	double x = (rand()%1000)/1000;
      	double y = (rand()%1000)/1000;
      	
      	if(x*x + y*y < 1){
      	  count ++;
      	}
      }
      return 4.0*count/n;
    }'
  end
end

n = ARGV[0].to_i
startTime = Time.new
CalcPi.new.withInlineC(n)
puts "Processing withInlineC took #{ Time.new - startTime } seconds."

Das Codeschnipsel erzeugt eine neue Instanzmethode withInlineC, die Sie aus dem restlichen Programm wie gewohnt aufrufen können. Typkonvertierungen werden automatisch vorgenommen. Aufgrund der kompillierten Eigenschaften von C, sowie der strengen Typisierung und hochoptimierter Compiler ist dieser Code um eine Größenordnung schneller als der reine Ruby Code und erzeugt auf dem Testsystem folgende Ausgabe: Processing withInlineC took 0.307251781 seconds.. Der Nachteil liegt in einer längeren Ladezeit des Skriptes, da der Inlinecode erst kompiliert werden muss. Diese Variante lohnt sich nur, falls es sich um eine kritische Komponente handelt, die während der Laufzeit des Programms oft benutzt wird, so dass sich die anfänglich größere Ladezeit lohnt.

C Erweiterung[Bearbeiten]

Das Schreiben einer C-Erweiterung sollte genau überdacht werden, denn obwohl C eine sehr schnelle Sprache ist bringt es einige Probleme mit sich und es ist aufgrund niedriger Abstraktion sehr viel schwieriger funktionierenden Code zu erzeugen. Trotzdem soll an dieser Stelle auf die Möglichkeit und deren Umsetzung hingewiesen werden. Da es an einigen Stellen nötig werden kann, dass Sie eine Erweiterung in C schreiben müssen, zum Beispiel wenn Sie auf bestehende C-Programme und Bibliotheken zurückgreifen müssen.

Zunächst der C-Quelltext, der wie in den beiden Beispielen oben eine Klasse CalcPi erzeugt mit zwei Instanzmethoden: initialize und withCExtension.

//calcPi.c

#include <ruby.h>

static VALUE t_withCExtension(VALUE self, VALUE n) {
  // Führt Konvertierung zwischen der Rubydarstellung von n und einer C-internen Darstellung durch.
  int limit = NUM2INT(n);
  int count = 0;
  int i;
  for(i = 0; i <= limit; i++) {
    double x = (rand()%1000)/1000.0;
    double y = (rand()%1000)/1000.0;
      	
    if(x*x + y*y <= 1.0){
      count++;
    }
  }
  
  // Erzeugt eine neue Ruby-Gleitkommazahl und gibt diese an das aufrufende Programm.    
  return rb_float_new(4.0 * count / limit);
}


static VALUE t_init(VALUE self) {
	return self;
}

// Diese Funktion wird vom Rubyinterpreter aufgerufen, wenn er die Erweiterung lädt, dabei muss wie in diesem Fall calcPi dem Dateinamen entsprechen.
void Init_calcPi() {
  // Erzeugt die Klasse CalcPi, deren Elternklasse Object ist.
  VALUE calcPi = rb_define_class ("CalcPi", rb_cObject);
  
  // Erzeugt die Methoden
  rb_define_method(calcPi, "withCExtension", t_withCExtension, 1);
  rb_define_method(calcPi, "initialize", t_init, 0);
}

Nachdem Sie diesen Quelltext unter dem Namen calcPi.c gespeichert haben. Müssen Sie diesen kompillieren, eine Bibliothek, die diese Aufgabe übernimmt ist mkmf (make makefile). Konventionell ist es eine Datei extconf.rb anzulegen mit folgendem minimalen Inhalt:

require 'mkmf'
create_makefile('calcPi')

Nachdem ausführen dieses Skriptes und dem starten von make wurde eine Datei erstellt mit dem Namen calcPi.so (unter Linux, je nach Betriebssystem auch .dll oder .bundle). Diese kann nun mittels require 'calcPi' in Rubyskripten verwendet werden.

require './calcPi'

n = ARGV[0].to_i

startTime = Time.new
puts CalcPi.new.withCExtension(n)
puts "Processing withCExtension took #{ Time.new - startTime } seconds."
startTime = Time.new

Die Erweiterung muss jetzt nicht beim laden des Skriptes kompilliert werden, dadurch reduziert sich die Ladezeit gegenüber der Inlinemethode. Nachteile sind das komplizierte Interface und die vielen Fallstricke aufgrund der Verwendung von C (die natürlich bei der Inlinemethode auch auftreten). Zur Laufzeit ergeben sich gegenüber der Inlinemethode kaum Geschwindigkeitsvorteile, sondern die getestete Ausführungszeit liegt im selben Bereich: Processing withCExtension took 0.291250096 seconds.

Zusammenfassung[Bearbeiten]

Prinzipiell sollte das Nutzen einer nativen Bibliothek genau überdacht werden, weil es sich um mehr Aufwand handelt als eine vergleichbare Implementation in Ruby. Falls Sie in ihren Programmen auf Geschwindigkeitsprobleme stoßen, sollten Sie zunächst zu andere Mitteln greifen als die Funktionen in C umzuschreiben. Überprüfen Sie genau welche Programmteile kritisch sind, wo also viel zeit verbraucht wird und prüfen Sie ob Sie diese Bereiche verbessern können. Das kann das Zwischenspeichern von Berechnungen umfassen, oder das Wechseln eines Algorithmus. Je nach Anwendung kann es auch danach noch zu Engpässen kommen, dann kann es Vorteilhaft sein das gesamte Programm modularer zu gestallten und Programmteile in anderen Programmiersprachen zu schreiben. Als Stichwort soll hier Service Oriented Architecture erwähnt sein. Das löst zwar nicht die Probleme, die durch das Verwenden mehrerer Programmiersprachen entstehen, aber erlaubt es die Programmteile als abgeschlossene Teile zu betrachten.

Auf der anderen Seite ist das Verwenden von C-Code relativ einfach möglich, wenn Sie also C-Bibliotheken finden die nützliche Funktionalität bereitstellen, dann können Sie diese Nutzen und müssen keinen Arbeit darauf verwenden diese Funktionen neu zu schreiben.