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

Performancemessungen in .NET

Geschrieben von: Christoph Wille
Kategorie: .NET Allgemein

This printed page brought to you by AlphaSierraPapa

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!

This printed page brought to you by AlphaSierraPapa

Download des Codes

Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20011206.zip

Verwandte Artikel

Der ODBC .NET Data Provider
http:/www.aspheute.com/artikel/20020206.htm
Einführung: C#-Klassen in ASP.NET
http:/www.aspheute.com/artikel/20001012.htm
Formularbasierte Authentifizierung in fünf Minuten
http:/www.aspheute.com/artikel/20020705.htm
Performance Monitoring a la .NET
http:/www.aspheute.com/artikel/20000809.htm
SQL Injection
http:/www.aspheute.com/artikel/20011030.htm
Stored Procedures 101 in ADO.NET
http:/www.aspheute.com/artikel/20010626.htm
Tracing in ASP.NET
http:/www.aspheute.com/artikel/20001006.htm
Verhinderung von SQL Injection Marke .NET
http:/www.aspheute.com/artikel/20011203.htm

 

©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.