Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Datenbanken (https://www.delphipraxis.net/15-datenbanken/)
-   -   Vergleich Firebird: C# vs. Delphi Win32 (https://www.delphipraxis.net/163273-vergleich-firebird-c-vs-delphi-win32.html)

Morphie 22. Sep 2011 14:00

Datenbank: Firebird • Version: 2.5 • Zugriff über: ADO.NET / IBDAC

Vergleich Firebird: C# vs. Delphi Win32
 
Hi, ich lerne gerade C# und wollte einen Datenimport für Firebird programmieren.
Leider musste ich feststellen, dass meine C#-Lösung ca. 8x langsamer ist als meine Delphi-Lösung.

Habt ihr eine Idee, was ich hier falsch mache?

Habe mal ein Beispielprojekt gemacht.

Delphi-Code:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var
  Con: TIBCConnection;
  Qry: TIBCQuery;
  i: integer;
const
  max = 10000;
begin
  Con := TIBCConnection.Create(nil);
  try

    Con.Server := 'localhost';
    Con.Username := 'SYSDBA';
    Con.Password := 'masterkey';
    Con.Database := 'F:\Test.fdb';
    Con.Open;

    Qry := TIBCQuery.Create(nil);
    try
      Qry.Connection := Con;
      Qry.Params.CreateParam(ftInteger,'Par1',ptInput);
      Qry.Params.CreateParam(ftFloat,'Par2',ptInput);
      Qry.Params.CreateParam(ftString,'Par3',ptInput);
      Qry.Params.CreateParam(ftDateTime,'Par4',ptInput);
      Qry.SQL.Text := 'UPDATE OR INSERT INTO TEST (FELD1, FELD2, FELD3, FELD4) VALUES (:Par1, :Par2, :Par3, :Par4);';
      Qry.Prepare;

      ProgressBar1.Max := max;
      ProgressBar1.Position := 0;

      for i := 0 to max do
      begin
        Qry.ParamByName('Par1').Value := i;
        Qry.ParamByName('Par2').Value := i;
        Qry.ParamByName('Par3').Value := 'Test';
        Qry.ParamByName('Par4').Value := Now;
        Qry.Execute;

        ProgressBar1.Position := i;
        Label1.Caption := inttostr(i) + ' / ' + inttostr(max);
        Application.ProcessMessages;
      end;

    finally
      qry.Free;
    end;

  finally
    Con.Free;
  end;
end;
C#-Code:
Code:
private void button2_Click(object sender, EventArgs e)
{
    FbConnection Con = new FbConnection();
    FbConnectionStringBuilder ConStrBuilder = new FbConnectionStringBuilder();

    // ConnectionString bauen
    ConStrBuilder.DataSource = "localhost";
    ConStrBuilder.UserID = "SYSDBA";
    ConStrBuilder.Password = "masterkey";
    ConStrBuilder.Database = "F:\\Test.fdb";
               
    // ConnectionString zuweisen
    Con.ConnectionString = ConStrBuilder.ConnectionString;
    Con.Open();

    FbCommand Cmd = new FbCommand();
    Cmd.Connection = Con;
    Cmd.Parameters.Add("Par1", FbDbType.Integer);
    Cmd.Parameters.Add("Par2", FbDbType.Decimal);
    Cmd.Parameters.Add("Par3", FbDbType.VarChar);
    Cmd.Parameters.Add("Par4", FbDbType.TimeStamp);
    Cmd.CommandText = "UPDATE OR INSERT INTO TEST (FELD1, FELD2, FELD3, FELD4) VALUES (@Par1, @Par2, @Par3, @Par4);";
    Cmd.Prepare();

    const int max = 10000;

    progressBar1.Maximum = max;
    progressBar1.Value = 0;

    for (int i = 0; i < max; i++)
    {
        Cmd.Parameters["Par1"].Value = i;
        Cmd.Parameters["Par2"].Value = i;
        Cmd.Parameters["Par3"].Value = "Test";
        Cmd.Parameters["Par4"].Value = DateTime.Now;
        Cmd.ExecuteNonQuery();

        progressBar1.Value = i;
        label2.Text = i.ToString() + " / " + max.ToString();
        Application.DoEvents();
    }
}
Ich habe auch schon Transaktionen eingebaut, das hat allerdings auch nichts gebracht.

Die Tabellendefinition sieht so aus:
Code:
CREATE TABLE TEST (
  FELD1  INTEGER NOT NULL,
  FELD2  DECIMAL,
  FELD3  VARCHAR(20),
  FELD4  TIMESTAMP,
  /* Schlüssel */
  PRIMARY KEY (FELD1)
);
Folgende Werte konnt ich mittels GetTickCount (sind das eig. Millisekunden?) ermitteln:
Code:
Versuch   Delphi   C#
1          8515      68312
2          8297      55312
3          8703      54969
4          9984      53969
5          8671      54218

Elvis 22. Sep 2011 14:08

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Das DoEvents muss in Winforms mehr machen, als in Delphi.
Hat da auch gar nix zu suchen. In Winforms kannst du den BackgroundWorker nehmen wenn du ohne viel Arbeit etwas im Hintergund erledigen willst.
DoEvents ist nur dafür da um WinForms zu zwingen JETZT die MessageQueue abzuarbeiten, aber nicht um die UI bedienbar zu machen.
So schnell wie in Delphi kann das aber nicht werden. Denn der firebird Client ist eine native DLL, und Calls in native DLLs müssen ihre Daten ständig von & zu .Net übersetzen. Das kostet einfach ein bissel was.

Wenn du mehr zu Threading in C# wissen willst, schaue die die TPL an.

Morphie 22. Sep 2011 14:17

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Oh, habe vergessen zu sagen, dass das DoEvents bzw. ProcessMessages erst später dazu kam, um die Anzeige zu aktualisieren. Es macht also keinen großen Unterschied ob mit oder ohne DoEvents.
Ist ja auch nur ein Beispielprogramm...
Dass DoEvents gar nicht benötigt wird, wusste ich nicht. Tut einfach so als wäre es nich da ;-)

tsteinmaurer 22. Sep 2011 14:23

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Weil du Transaktionen erwähnt hast. War das eine explizit gestartete Transaktion, in der dann alles lief? Weil deine Beispiele sehen nach AutoCommit-Modus aus, d.h. nach jedem Query-Execute wird ein Commit(Retaining) gemacht.

Morphie 22. Sep 2011 14:36

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Eine Transaktion die explizit gestartet wird:
Code:
private void button2_Click(object sender, EventArgs e)
{
    FbConnection Con = new FbConnection();
    FbConnectionStringBuilder ConStrBuilder = new FbConnectionStringBuilder();

    // ConnectionString bauen
    ConStrBuilder.DataSource = "localhost";
    ConStrBuilder.UserID = "SYSDBA";
    ConStrBuilder.Password = "masterkey";
    ConStrBuilder.Database = "F:\\Test.fdb";
               
    // ConnectionString zuweisen
    Con.ConnectionString = ConStrBuilder.ConnectionString;
    Con.Open();

    FbTransaction Trans = Con.BeginTransaction();

    FbCommand Cmd = new FbCommand();
    Cmd.Connection = Con;
    Cmd.Transaction = Trans;
    Cmd.Parameters.Add("Par1", FbDbType.Integer);
    Cmd.Parameters.Add("Par2", FbDbType.Decimal);
    Cmd.Parameters.Add("Par3", FbDbType.VarChar);
    Cmd.Parameters.Add("Par4", FbDbType.TimeStamp);
    Cmd.CommandText = "UPDATE OR INSERT INTO TEST (FELD1, FELD2, FELD3, FELD4) VALUES (@Par1, @Par2, @Par3, @Par4);";
    Cmd.Prepare();

    const int max = 5000;

    progressBar1.Maximum = max;
    progressBar1.Value = 0;

    for (int i = 0; i < max; i++)
    {
        Cmd.Parameters["Par1"].Value = i;
        Cmd.Parameters["Par2"].Value = i;
        Cmd.Parameters["Par3"].Value = "Test";
        Cmd.Parameters["Par4"].Value = DateTime.Now;
        Cmd.ExecuteNonQuery();

        progressBar1.Value = i;
        label2.Text = i.ToString() + " / " + max.ToString();
        Application.DoEvents();
    }
    Trans.Commit();
}

Phoenix 22. Sep 2011 14:38

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Morphie (Beitrag 1126030)
Folgende Werte konnt ich mittels GetTickCount (sind das eig. Millisekunden?) ermitteln:

Erm.. du bist da ein wenig off in .NET:
Zitat:

Zitat von MSDN
A single tick represents one hundred nanoseconds or one ten-millionth of a second. There are 10,000 ticks in a millisecond.

Das heisst das ganze Ding braucht da ca. 5,5 Sekunden. Das ist tatsächlich ziemlich langsam für nur 10.000 inserts.

Morphie 22. Sep 2011 14:49

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Phoenix (Beitrag 1126035)
Das heisst das ganze Ding braucht da ca. 5,5 Sekunden. Das ist tatsächlich ziemlich langsam für nur 10.000 inserts.

Habe mal mitgezählt. Es sind 50 Sekunden bei 5000 Datensätze!?

Phoenix 22. Sep 2011 14:58

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Vergiss das mitzählen.
Nimm eine Stopwatch, die ist genau für solche Zeitmessungen da und ist dafür sehr exakt.

Erstmal eine komplett drumrum.

Dann am besten zwei Stopwatches:
Die eine ums komplette rum, die zweite stoppst Du in jedem Durchlauf, schreibst die elapsed time z.B. in eine List<TimeSpan>, resettest und startest sie neu.

Dann hast Du auch ne Info wie lange ein insert dauert.
Du kannst auch alternativ während die Stopwatch läuft ab und zu die elapsed time auslesen und z.B: auf die Debug- oder Trace-Klasse schreiben.
Dann siehst Du am ehesten bei welchen calls die meiste Zeit verloren geht.

Aber aufpassen: Wenn Du das im Debug-Build und im worst case noch mit attachtem debugger laufen lässt wird das dann *richtig* langsam.

Satty67 22. Sep 2011 16:31

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Phoenix (Beitrag 1126040)
im worst case noch mit attachtem debugger laufen lässt wird das dann *richtig* langsam.

...oder anders ausgedrückt Zeitmessung mit Strg-F5 (statt F5)

Morphies Beispiel-Code ist bei mir ohne Debugger ziemlich genau um den Faktor 8 schneller.

(ohne string + string + string und DoEvents bei jedem Durchlauf sollte es vergleichbar schnell wie mit Delphi sein)

Phoenix 22. Sep 2011 18:58

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Satty67 (Beitrag 1126055)
Morphies Beispiel-Code ist bei mir ohne Debugger ziemlich genau um den Faktor 8 schneller.

(ohne string + string + string und DoEvents bei jedem Durchlauf sollte es vergleichbar schnell wie mit Delphi sein)

Naja, das
Code:
label2.Text = i.ToString() + " / " + max.ToString();
ist eine einzige Konkatenation und erzeugt lediglich eine neue String-Variable. Das sollte verschmerzbar sein und nicht stören. Am besten packt er das ganze in einen Background-Thread (wurde schon angesprochen), die Label - Aktualisierung dann in eine jeweils neue anonyme Methode und schmeisst die dann zum Invoken auf den Mainthread. So hat die DB-Geschichte einen kompletten Kern für sich und das UI darf versuchen hinter den ganzen Aktualisierungen herzulaufen ;-)

Morphie 22. Sep 2011 19:07

AW: Vergleich Firebird: C# vs. Delphi Win32
 
wie gesagt, das war nur eine Testanwendung ;) Die eigentliche Anwendung ist natürlich sauberer...
Zu euren Vorschlägen:
Mit Strg + F5 läuft das ganze schon wesentlich schneller! Das war anscheinend das Kernproblem... :-)

Jetzt bin ich mit 10000 Datensätze bei ziemlich genau 4 Sekunden. Ich denke das reicht soweit.
Delphi braucht bei entsprechendem Code nun ca. 2,2 Sekunden.

Vielen Dank :)

Phoenix 22. Sep 2011 19:12

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Morphie (Beitrag 1126086)
Zu euren Vorschlägen:
Mit Strg + F5 läuft das ganze schon wesentlich schneller! Das war anscheinend das Kernproblem... :-)

Ja, der Debugger analysiert ne Menge mit im Hintergrund. Performancetests daher immer ohne Debugger machen. Und die 2 Sekunden Differenz bekommst Du auch noch raus ;-)

Morphie 22. Sep 2011 20:26

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Hast du da zufällig einen Tipp wie ich das machen könnte?
Klar, irgendwann wird das in einen Thread ausgelagert, aber von der Ausführungszeit sollte das doch noch keinen Unterschied machen...

Die Schleife sieht nun so aus:
Code:
const int max = 10000; // < Anzahl
for (int i = 0; i < max; i++)
{
    // Werte den Parametern zuweisen
    Cmd.Parameters["Par1"].Value = i;
    Cmd.Parameters["Par2"].Value = i;
    Cmd.Parameters["Par3"].Value = "Test";
    Cmd.Parameters["Par4"].Value = DateTime.Now;

    // Command durchführen
    Cmd.ExecuteNonQuery();

    // Nach jeweils 3000 Datensätz die Transaktion "committen"
    if ((i % 3000) == 0) Trans.CommitRetaining();
}
wüsste nicht, was ich da noch optimieren könnte...

Phoenix 23. Sep 2011 06:32

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Zitat:

Zitat von Morphie (Beitrag 1126101)
wüsste nicht, was ich da noch optimieren könnte...

Ich schon ;-)
Der Indexer-Zugriff auf den Parameter muss nicht innerhalb der Schleife passieren, weil das jedesmal ein Loopup ist. Das reicht wenn man den ein einziges mal macht, und nicht 40.000 mal :)

Code:
var param1 = Cmd.Parameters["Par1"];
var param2 = Cmd.Parameters["Par2"];
var param3 = Cmd.Parameters["Par3"];
var param4 = Cmd.Parameters["Par4"];

const int max = 10000; // < Anzahl
for (int i = 0; i < max; i++)
{
    // Werte den Parametern zuweisen
    param1.Value = i;
    param2.Value = i;
    param3.Value = "Test";
    param4.Value = DateTime.Now;

    // Command durchführen
    Cmd.ExecuteNonQuery();

    // Nach jeweils 3000 Datensätz die Transaktion "committen"
    if ((i % 3000) == 0) Trans.CommitRetaining();
}
Alternativ kannst Du Dir die Referenzen auf die Parameter auch gleich beim Erzeugen der Parameter merken, dann sparst Du Dir die 4 Lookups zu Beginn auch nochmal, da wär allerdings wirklich mikrooptimierung.

tsteinmaurer 23. Sep 2011 07:02

AW: Vergleich Firebird: C# vs. Delphi Win32
 
@Morphie: Wird nicht mehr so viel bringen, aber anstatt CommitRetaining ein normales Commit machen. Somit ist es aus Sicht von Firebird keine langlaufende Transaktion und die OIT/OAT kann nachziehen.

Morphie 23. Sep 2011 07:29

AW: Vergleich Firebird: C# vs. Delphi Win32
 
Danke =)
Beide Tipps zusammen bringen ca. 100ms bei 10.000 DS.

So lasse ich es nun erstmal denke ich...


Alle Zeitangaben in WEZ +1. Es ist jetzt 20:36 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz