Glengamoi (Forum) · AspHeute · .NET Heute (RSS-Suche) · AspxFiles (Wiki) · .NET Blogs
ASP German Homepage Homepage
 

Liste

.NET 2.0 (1)
.NET Allgemein (16)
.NET Fu (5)
ADO.NET (11)
Aprilscherz (3)
ASP Grundlagen (44)
ASP Tricks (83)
ASP.NET (44)
ASPIntranet.de (5)
C# (28)
Datenbank (44)
Dokumentation (4)
IIS 6.0 (1)
Komponenten (29)
Optimierung (10)
Server (21)
Sicherheit (34)
Tee Off (6)
VB.NET (6)
WAP (8)
Web Services (11)
XML (9)

RSS 2.0 - Die neuesten fünf Artikel auf AspHeute.com


 

Suchen





 

English Articles
Chinese Articles
Unsere Autoren
 
Link zu AspHeute
Impressum
Werben
Anfragen

Performancemessungen in .NET

Geschrieben von: Christoph Wille
Kategorie: .NET Allgemein

Heute beschäftigen wir uns mit einem altbekannten Thema, der Messung der (Ausführungs-)Geschwindigkeit einer bestimmten Implementierung. Dazu sehen wir uns vier verschiedene ADO.NET Implementierungen für ein und dasselbe Problem an, an denen ich auch wieder zeigen werde, daß so manches Mal der Schein trügen kann - Code kann auch langsam aussehen, obwohl er es nicht ist.

Als Aufgabenstellung errechne ich das Gesamtfrachtgewicht aller Bestellungen eines Kunden bei Northwind Traders. Netterweise sind alle Daten in der Orders Tabelle enthalten, und für uns sind die Spalten Freight und CustomerId von Interesse. Im Prinzip ist das SQL Statement für alle vier Arten der Implementierung fix vorgebenen:

select sum(freight) from orders where CustomerId='somecustomerid'

Das WHERE Statement ist klarerweise dynamisch aufzubauen. Die erste Variante der Implementierung sieht so aus (die SqlConnection ist bereits geöffnet):

Trace.Write("PerfMonitoring", "Start1");
SqlCommand cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders " + 
        " where CustomerId = '" + strTestCompany + "'";
cmd.Connection = scNWind;

object RetVal1 = cmd.ExecuteScalar();
cmd.Dispose();

Hier wird das SQL Statement dynamisch zusammengebaut (Achtung vor SQL Injection!), und die Summe der Frachtgewichte mittels ExecuteScalar in die Variable RetVal geschrieben.

Die bessere Variante ohne der Gefahr der SQL Injection ist die folgende:

cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders where CustomerId = @ParameterCustomerId";
SqlParameter param2 = cmd.Parameters.Add("@ParameterCustomerId", strTestCompany);
cmd.Connection = scNWind;

object RetVal2 = cmd.ExecuteScalar();
cmd.Dispose();

Das Konzept der parametrisierten Abfragen unter ADO.NET habe ich bereits im Artikel Verhinderung von SQL Injection Marke .NET besprochen. Also eigentlich ist dies die erste "korrekte" Implementierung unserer Problemstellung.

Nun aber zu Variante drei:

cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders where CustomerId = @ParameterCustomerId";
SqlParameter param3 = cmd.Parameters.Add("@ParameterCustomerId", strTestCompany);
cmd.Connection = scNWind;

SqlDataReader MySqlReader = cmd.ExecuteReader();
MySqlReader.Read();
object RetVal3 = MySqlReader.GetValue(0);
MySqlReader.Close();
cmd.Dispose();

Hier verwende ich ExecuteReader, um an einen SqlDataReader heranzukommmen. Aus diesem - und der ersten Spalte der ersten (und einzigen) Ergebnisspalte hole ich den Wert.

Last but not least - und das mußte kommen - eine Implementierung in einer Stored Procedure:

CREATE PROCEDURE sp_SumItUp 
	@CustomerId nvarchar(5),
	@TheSum float output
AS
select @TheSum = sum(freight) from orders where CustomerId=@CustomerId
GO

Der ADO.NET Code dazu sieht dann so aus:

cmd = new SqlCommand();
cmd.CommandText = "sp_SumItUp";
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter param4 = cmd.Parameters.Add("@CustomerId", SqlDbType.NVarChar, 5);
param4.Value = strTestCompany;
SqlParameter param5 = cmd.Parameters.Add("@TheSum", SqlDbType.Float);
param5.Direction = ParameterDirection.Output;
cmd.Connection = scNWind;

cmd.ExecuteNonQuery();
object RetVal4 = param5.Value;
cmd.Dispose();

Welche der vier Varianten ist am schnellsten? Jeder soll sich einen Favoriten heraussuchen, und dann erst weiterlesen!

Performancetest mittels Trace Statements

Klingt komisch, ist es aber nicht. Wer sich das Tracing in ASP.NET einmal genauer angeschaut hat, wird gesehen haben, daß die Zeitangaben hochgenau sind. Warum sollten wir das nicht für unsere Zwecke verwenden können?

Wir brauchen nur einige wenige zusätzliche Zeilen Code in unserem Performance-Meßscript (test.aspx):

<%@Page Language="C#" Debug="True" Trace="True" TraceMode="SortByCategory" %>
<%@Import Namespace="System.Data" %>
<%@Import Namespace="System.Data.SqlClient" %>
<%
string strTestCompany = "HANAR";

string strConn = "user id=sa;password=;initial catalog=northwind;...";
SqlConnection scNWind = new SqlConnection(strConn);
scNWind.Open();

// v1
Trace.Write("PerfMonitoring", "Start1");
SqlCommand cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders " + 
        " where CustomerId = '" + strTestCompany + "'";
cmd.Connection = scNWind;

object RetVal1 = cmd.ExecuteScalar();
cmd.Dispose();
Trace.Write("PerfMonitoring", "End1");

// v2
Trace.Write("PerfMonitoring", "Start2");
cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders where CustomerId = @ParameterCustomerId";
SqlParameter param2 = cmd.Parameters.Add("@ParameterCustomerId", strTestCompany);
cmd.Connection = scNWind;

object RetVal2 = cmd.ExecuteScalar();
cmd.Dispose();
Trace.Write("PerfMonitoring", "End2");

// v3
Trace.Write("PerfMonitoring", "Start3");
cmd = new SqlCommand();
cmd.CommandText = "select sum(freight) from Orders where CustomerId = @ParameterCustomerId";
SqlParameter param3 = cmd.Parameters.Add("@ParameterCustomerId", strTestCompany);
cmd.Connection = scNWind;

SqlDataReader MySqlReader = cmd.ExecuteReader();
MySqlReader.Read();
object RetVal3 = MySqlReader.GetValue(0);
MySqlReader.Close();
cmd.Dispose();
Trace.Write("PerfMonitoring", "End3");

// v4
Trace.Write("PerfMonitoring", "Start4");
cmd = new SqlCommand();
cmd.CommandText = "sp_SumItUp";
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter param4 = cmd.Parameters.Add("@CustomerId", SqlDbType.NVarChar, 5);
param4.Value = strTestCompany;
SqlParameter param5 = cmd.Parameters.Add("@TheSum", SqlDbType.Float);
param5.Direction = ParameterDirection.Output;
cmd.Connection = scNWind;

cmd.ExecuteNonQuery();
object RetVal4 = param5.Value;
cmd.Dispose();
Trace.Write("PerfMonitoring", "End4");

scNWind.Close();
%>
Im Prinzip sind es die Zeilen
Trace.Write("PerfMonitoring", "Start");
...
Trace.Write("PerfMonitoring", "End");

die die Zeitdifferenz für uns ausmessen (End - Start). Daraus erhält man dann folgendes Ergebnis:

Die richtige "Performance-Überraschung" ist der SqlDataReader Code (Variante 3). Er ist schneller als beide ExecuteScalar Varianten (1 und 2). Warum? Nun, ein Blick in den IL Assembler von Sql.Data.dll zeigt warum - intern wird auch der SqlDataReader verwendet, nur etliche Überprüfungen mehr durchgeführt. Ein Beweis, daß mehr Code nicht gleich langsamere Ausführung bedeuten muß.

Der Gewinner ist die Stored Procedure. Deutlicher würde der Abstand ausfallen, würde die Stored Procedure mehr Arbeit am Server durchführen (Arbeit, die die anderen Varianten am Client ausführen müßten).

Performancemessung ohne Trace Statements

Trace Statements schön und gut, aber üblicherweise misst man eine Codesektion mehrmals, und bildet dann über die Gesamtausführungszeit ein Mittel. Obwohl das mit Trace Statements auch noch so irgendwie ginge, sind nicht alle Applikation in ASP.NET geschrieben - es könnte ja auch sein, daß man eine Komponente testen möchte.

Hierzu wäre es dann geschickt, wenn man die Stoppuhr-Funktionalität der Trace Statements irgendwie nachbilden könnte. Ich habe mir diese Arbeit gemacht, und folgende Klasse erstellt:

using System;
using System.Runtime.InteropServices;

public class PerfTiming
{
 [DllImport("KERNEL32")]
 public static extern bool QueryPerformanceCounter(ref Int64 nPfCt);  

 [DllImport("KERNEL32")]
 public static extern bool QueryPerformanceFrequency(ref Int64 nPfFreq);

 protected Int64 m_i64Frequency;
 protected Int64 m_i64Start;

 public PerfTiming()
 {
  QueryPerformanceFrequency(ref m_i64Frequency);
  m_i64Start = 0;
 }

 public void Start()
 {
  QueryPerformanceCounter(ref m_i64Start);  
 }

 public double End()
 {
  Int64 i64End=0;
  QueryPerformanceCounter(ref i64End);
  return ((i64End - m_i64Start)/(double)m_i64Frequency);
 }
}

Dieser Code bedient sich zweier Funktionen aus dem WIN32 API, die einen hochgenauen Timer abbilden. QueryPerformanceFrequency liefert mir die Anzahl der Ticks pro Sekunde (Frequenz also), wohingegen QueryPerformanceCounter den aktuellen Tickwert liefert. Dividiert man die Differenz Tickendwert und Tickstartwert durch die Frequenz, erhält man die Zeitdifferenz in Sekunden. In einem Programm sieht das dann so aus:

 PerfTiming pt = new PerfTiming();
 pt.Start();
    Console.WriteLine("Test"); // eigentlich sollte das mehr Code sein...
 double dTimeTaken = pt.End();
 Console.WriteLine(dTimeTaken.ToString());

Die Zeitmessung wird mit Start ausgelöst, und End liefert die Zeit in Sekunden als Double Wert. Damit kann man nun elegant und einfach Ausführungszeiten in beliebigen .NET Anwendungen messen, als Komponente kompiliert ist die Klasse natürlich jeder beliebigen Programmiersprache zugänglich.

Schlußbemerkung

Anhand von vier Implementierungen eines ADO.NET Problems haben wir uns heute angesehen, wie man mit Bordmitteln von .NET (oder WIN32) die Performance extrem genau ermitteln kann. Ein weiterer wichtiger Punkt des heutigen Artikels: die Länge des Codes sagt nichts über die Performance aus!

Download des Codes

Klicken Sie hier, um den Download zu starten.

Verwandte Artikel

Der ODBC .NET Data Provider
Einführung: C#-Klassen in ASP.NET
Formularbasierte Authentifizierung in fünf Minuten
Performance Monitoring a la .NET
SQL Injection
Stored Procedures 101 in ADO.NET
Tracing in ASP.NET
Verhinderung von SQL Injection Marke .NET

Wenn Sie jetzt Fragen haben...

Wenn Sie Fragen rund um die in diesem Artikel vorgestellte Technologie haben, dann schauen Sie einfach bei uns in den Community Foren der deutschen .NET Community vorbei. Die Teilnehmer helfen Ihnen gerne, wenn Sie sich zur im Artikel vorgestellten Technologie weiterbilden möchten.

Eine weitere sehr hilfreiche Resource ist das deutsche ASP.NET Wiki, das als zentrale Anlaufstelle für Tips, Tricks, Know How und alles Nützliche was man in seinem Alltag als (ASP).NET-Entwickler so braucht und entdeckt gedacht ist.

Haben Sie Fragen die sich direkt auf den Inhalt des Artikels beziehen, dann schreiben Sie dem Autor! Unsere Autoren freuen sich über Feedback zu ihren Artikeln. Ein einfacher Klick auf die Autor kontaktieren Schaltfläche (weiter unten) und schon haben Sie ein für diesen Artikel personalisiertes Anfrageformular.

 

Und zu guter Letzt möchten wir Sie bitten, den Artikel zu bewerten. Damit helfen Sie uns, die Qualität der Artikel zu verbessern - und anderen Lesern bei der Auswahl der Artikel, die sie lesen sollten.

Bewerten Sie diesen Artikel
 Sehr gut   Nicht genügend  
   1  2  3  4  5  
 

  
   Für Ausdruck optimierte Seite

©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.