我是一个休闲的C#程序员,根本没有接受过OOP的正式培训。我主要专注于SQL Server中的Transact-SQL,因此,当我编写C#应用程序时,我会对使用的结构和方法感到悲伤。这是我为该博客文章编写的一个简单应用程序-并不是要成为生产应用程序,性能当然不是优先考虑的事情,但是我想自然地变得更好地编写最佳代码。

代码的用途非常简单-对于100,000个周期,我需要从SQL Server存储过程中生成一个随机名称和数字,然后将这些值写入表中。之后,从表中读取100个随机行,进行1000次,并且并不真正在乎您遍历100行的操作。

练习的重点是分析加密对应用程序。因此,阅读器循环所执行的操作实际上并不重要,也不需要对其进行优化,因为这只是占用时间,并且在两种情况下都是相同的。还应注意的是,多个连接是有意的-应该进行模拟-无需进入多线程或应用的多个实例-在高度并发的应用中您会看到的所有开销(即使深入了解,它仍然按顺序执行。)

向我指出的项目(我想更好地理解):



for优于while

顶部的可变声明是错误的
代码Code肿
可移植性很差

我也肯定有更好的方法计时代码的性能,而不是将当前时钟时间转储到控制台。

using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;

namespace AEDemo
{
  class Program
  {
    static void Main(string[] args)
    {
      using (SqlConnection con1 = new SqlConnection())
      {
        Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
        string name;
        string EmptyString = "";
        string conString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
        int salary;
        int i = 1;
        while (i <= 100000)
        {
          con1.ConnectionString = conString;
          using (SqlCommand cmd1 = new SqlCommand("dbo.GenerateNameAndSalary", con1))
          {
            cmd1.CommandType = CommandType.StoredProcedure;
            SqlParameter n = new SqlParameter("@Name", SqlDbType.NVarChar, 32) 
                             { Direction = ParameterDirection.Output };
            SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int) 
                             { Direction = ParameterDirection.Output };
            cmd1.Parameters.Add(n);
            cmd1.Parameters.Add(s);
            con1.Open();
            cmd1.ExecuteNonQuery();
            name = n.Value.ToString();
            salary = Convert.ToInt32(s.Value);
            con1.Close();
          }

          using (SqlConnection con2 = new SqlConnection())
          {
            con2.ConnectionString = conString;
            using (SqlCommand cmd2 = new SqlCommand("dbo.AddPerson", con2))
            {
              cmd2.CommandType = CommandType.StoredProcedure;
              SqlParameter n = new SqlParameter("@LastName", SqlDbType.NVarChar, 32);
              SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int);
              n.Value = name;
              s.Value = salary;
              cmd2.Parameters.Add(n);
              cmd2.Parameters.Add(s);
              con2.Open();
              cmd2.ExecuteNonQuery();
              con2.Close();
            }
          }
          i++;
        }
        Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
        i = 1;
        while (i <= 1000)
        {
          using (SqlConnection con3 = new SqlConnection())
          {
            con3.ConnectionString = conString;
            using (SqlCommand cmd3 = new SqlCommand("dbo.RetrievePeople", con3))
            {
              cmd3.CommandType = CommandType.StoredProcedure;
              con3.Open();
              SqlDataReader rdr = cmd3.ExecuteReader();
              while (rdr.Read())
              {
                EmptyString += rdr[0].ToString();
              }
              con3.Close();
            }
          }
          i++;
        }
        Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
      }
    }
  }
}


评论

出于好奇,这平均需要执行多长时间?

@EBrown进行了5次以上的平均值:cdn.sqlperformance.com/wp-content/uploads/2015/08/AE-Perf.png

我想这个标题还可以吗?

大声笑@AaronBertrand,您应该在聊天中与他讨论。但是该标题非常简短地说明了代码的含义。

@Malachi我的原标题也是如此。

#1 楼

如果同时出现两个while循环,则可以轻松地使它们成为for循环,这通常是最佳做法。

    int i = 1;
    while (i <= 100000)
    {
        // main while code
        i++
    }


应重写为:

for (int i = 1; i <= 100000; i++)
{
    // main while code
}


类似:

    i = 1;
    while (i <= 1000)
    {
        // main while code
        i++;
    }


将成为:

for (int i = 1; i <= 1000; i++)
{
    // main while code
}


这使您可以在以后循环使用变量i,并且还可以避免在循环之间意外忘记将i重置回1。通常,变量i代表iterator,并且通常专门为循环保留。

这对性能没有实际影响,但对可读性和可维护性有严重影响。使用for循环,可以立即清楚迭代器发生了什么。使用while循环,您必须在while的主体中进行搜索,以找到在何处操纵迭代器。


>顶部的变量声明是错误的


通常对此不满意。您应该声明变量,使其尽可能接近其用法,并在要使用它们的最严格的范围内。例如,



   int salary;
   string name;



应该在第一个循环中声明(现在应该是for循环):

for (int i = 1; i <= 100000; i++)
{
    int salary;
    string name;
}


变量不能在不应使用的地方意外地使用,因此当它们立即超出范围时可以将其回收。这也意味着您不能在应用于它们的块之外意外地使用它们的值。


尝试选择更有效的变量名。除某些公认的缩写外(从iiteratoreeventArgs等),切勿缩写:

  using (SqlConnection con1 = new SqlConnection())
  {


将更被接受为: br />
  using (SqlConnection connection1 = new SqlConnection())
  {


这通常是最佳实践。


您可能会直接使用EmptyString变量看到巨大的性能下降:string类型是不可变的,这意味着每次执行EmptyString += rdr[0].ToString();时,它将使用组合创建字符串的新实例,然后将其分配给EmptyString,然后进行调度用于垃圾回收的原始字符串。

,而是使用StringBuilder

StringBuilder emptyStringBuilder = new StringBuilder();

for (int i = 1; i <= 1000; i++)
{
    // ...
    while (rdr.Read())
    {
        emptyStringBuilder.Append(rdr[0].ToString());
    }
    // ...
}


这样可以节省很多性能。有关更多信息,请参见MSDN。


另一个变量命名为nit-pick:您应该始终camelCase本地名称和private变量名称:即emptyString而不是EmptyString

此是另一种最佳实践。


我们全部重写后:

using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;

namespace AEDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SqlConnection connection1 = new SqlConnection())
            {
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
                string connectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString();

                for (int i = 1; i <= 100000; i++)
                {
                    connection1.ConnectionString = connectionString;

                    string name;
                    int salary;

                    using (SqlCommand command1 = new SqlCommand("dbo.GenerateNameAndSalary", connection1))
                    {
                        command1.CommandType = CommandType.StoredProcedure;
                        SqlParameter nameParameter = new SqlParameter("@Name", SqlDbType.NVarChar, 32) { Direction = ParameterDirection.Output };
                        SqlParameter salaryParameter = new SqlParameter("@Salary", SqlDbType.Int) { Direction = ParameterDirection.Output };

                        command1.Parameters.Add(nameParameter);
                        command1.Parameters.Add(salaryParameter);

                        connection1.Open();
                        command1.ExecuteNonQuery();
                        name = nameParameter.Value.ToString();
                        salary = Convert.ToInt32(salaryParameter.Value);
                        connection1.Close();
                    }

                    using (SqlConnection connection2 = new SqlConnection())
                    {
                        connection2.ConnectionString = connectionString;
                        using (SqlCommand command2 = new SqlCommand("dbo.AddPerson", connection2))
                        {
                            command2.CommandType = CommandType.StoredProcedure;
                            SqlParameter nameParameter = new SqlParameter("@LastName", SqlDbType.NVarChar, 32);
                            SqlParameter salaryParameter = new SqlParameter("@Salary", SqlDbType.Int);
                            nameParameter.Value = name;
                            salaryParameter.Value = salary;

                            command2.Parameters.Add(nameParameter);
                            command2.Parameters.Add(salaryParameter);

                            connection2.Open();
                            command2.ExecuteNonQuery();
                            connection2.Close();
                        }
                    }
                }
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));

                // We'll declare the StringBuilder outside the `for` to get maximum performance impact, and demonstrate it's effectiveness
                StringBuilder emptyStringBuilder = new StringBuilder();

                for (int i = 1; i <= 1000; i++)
                {
                    using (SqlConnection connection3 = new SqlConnection())
                    {
                        connection3.ConnectionString = connectionString;
                        using (SqlCommand command3 = new SqlCommand("dbo.RetrievePeople", connection3))
                        {
                            command3.CommandType = CommandType.StoredProcedure;
                            command3.Open();

                            SqlDataReader reader = command3.ExecuteReader();
                            while (reader.Read())
                            {
                                emptyStringBuilder.Append(reader[0].ToString());
                            }
                            connection3.Close();
                        }
                    }
                }
                Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
            }
        }
    }
}



免责声明

我在IDE之外编写了此代码,可能需要进行一些细微的更改才能使其正常工作。




另外,我相信还有更好的方法除了将当前时钟时间转储到控制台之外,还可以对代码的性能进行计时。是的,您应该查看Stopwatch类以进行计时。这比使用打印到DateTime上的Console字符串要精确得多。它还可以让您轻松地重置它,等等,因此您可以构建一个非常有效的诊断过程,以衡量您要尝试的特定性能影响。


我还建议您采取以下措施玛拉基(Malachi)关于连接字符串的建议。最后,在注释中进行讨论之后,我建议您构建一个类来表示dbo.RetrievePeople的返回值。然后,您应该创建该类的实例而不是emptyStringBuilder.Append,并将每个实例添加到List<T>。这将使您能够与实际用例进行更多的性能测试。


很高兴在这里看到您,您是DBA网站上的传奇人物! :)

评论


\ $ \ begingroup \ $
谢谢,迭代器的性能根本不是我要优化的。实际上,我故意写得很慢。
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
2015年8月12日14:00

\ $ \ begingroup \ $
迭代器的运行速度并不慢,将字符串添加到字符串部分是很慢的。根据您要测量的内容(明智的性能),最好在那里使用StringBuilder,这样就不会因多余资源的创建/回收而产生噪音。
\ $ \ endgroup \ $
– Der Kommissar
15年8月12日在14:01

\ $ \ begingroup \ $
好的,但是我正在尝试使应用程序执行某些操作,即使它很浪费,就像真实应用程序一样。
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
15年8月12日在14:04

\ $ \ begingroup \ $
@AaronBertrand问题是,您是否要测试它在应用程序或SQL Server上的性能?如果要对SQL Server进行压力测试,则最好使用StringBuilder,因为这样做会明显更快,然后几乎立即返回SQL Server进行更多工作。 (我认为那是你的目标?)
\ $ \ endgroup \ $
– Der Kommissar
15年8月12日在14:06

\ $ \ begingroup \ $
@AaronBertrand字符串串联具有二次性能损失。这并不代表实际的应用程序。我了解您需要进行一些处理,但是行数应该是线性的。不二次。
\ $ \ endgroup \ $
–usr
15年8月12日在14:28

#2 楼

为什么在using (SqlConnection con1 = new SqlConnection())循环之前的while循环中没有using (SqlCommand cmd1 = new SqlCommand("dbo.GenerateNameAndSalary", con1))

现在con1在第14行被实例化,直到第82行才被处理,但是在第39行之后才被使用。您实际上是在con2 con1块以及其中的using内部打开con3 while (i <= 1000)


此:

SqlParameter n = new SqlParameter("@LastName", SqlDbType.NVarChar, 32);
SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int);
n.Value = name;
s.Value = salary;
cmd2.Parameters.Add(n);
cmd2.Parameters.Add(s);


...可以重写为:

cmd2.Parameters.Add("@LastName", SqlDbType.NVarChar, 32).Value = name;
cmd2.Parameters.Add("@Salary", SqlDbType.Int).Value = salary;



为什么这样做:

using (SqlConnection con2 = new SqlConnection())
{
    con2.ConnectionString = conString;


...当SqlConnection具有接受连接字符串的构造函数时?


由于连接位于con1.Close();块内,因此无需执行con2.Close();using等。


您还需要将SqlDataReader包裹在using块内。

评论


\ $ \ begingroup \ $
我不知道如何错过没有using块的SqlDataReader。好答案!
\ $ \ endgroup \ $
– Der Kommissar
15年8月12日在14:24

#3 楼

您应该通过执行以下操作将连接字符串移出using语句之外

  using (SqlConnection con1 = new SqlConnection())
  {
    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
    string name;
    string EmptyString = "";
    string conString = ConfigurationManager.ConnectionStrings[args[0]].ToString();
    int salary;
    int i = 1;
    while (i <= 100000)
    {
      con1.ConnectionString = conString;


像这样

  string conString = ConfigurationManager.ConnectionStrings[args[0]].ToString();

  using (SqlConnection con1 = new SqlConnection(conString))
  {
    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
    string name;
    string EmptyString = "";
    int salary;
    int i = 1;
    while (i <= 100000)
    {


您无需再在代码中的后面的SqlConnection对象中设置连接字符串。

您也可以省去关闭连接的调用,这在范围离开using语句时自动发生。


我认为与数据库的连接与其余代码无关。实际上,您可以为该应用程序中的每个操作使用相同的连接,它应该使您更好地了解每个操作的处理方式。

我建议不要调用Console.WriteLine直到方法结束。我的意思是说,您应该将时间保存在变量中,并在所有乐趣开始之前进行声明,将变量分配给当前具有Console.WriteLine的位置,然后在完成所有操作后将其写到控制台中。

这将使Console.WriteLine繁重的过程从计算这些过程的时间的方程式中脱离出来。


还有一种方法可以使用围绕SqlDataReader的声明。

您必须创建阅读器,然后将其放入这样的using语句中。

SqlDataReader rdr = cmd3.ExecuteReader();
using (rdr)
{
    while (rdr.Read())
    {
        EmptyString += rdr[0].ToString();
    }
}


它与命令或命令稍有不同。连接,但数据读取器仍实现IDisposable接口,并且在使用中发生任何事情时都应正确处理。


就循环而言,for循环将看起来更整洁,并且更易于维护,如果以后需要在代码中进行一些更改。


在for循环声明中声明增量变量
在for循环声明中处理句柄增量
处理for循环声明中的循环数

使用while循环时,您只能在声明内部执行其中之一,其余操作必须在代码中的其他位置完成,并且可能导致意外的增量或其他更改增量变量的操作。

使用for循环,递增变量的作用域为该循环,并在作用域离开for循环时销毁。在while循环中,在退出while循环后,增量变量仍然可用,并且您不需要访问该值,为什么要这么做?

评论


\ $ \ begingroup \ $
您能解释一下为什么更好吗?记住,我想连接到数据库100,000次,而不是一次,因此握手开销是配置文件的一部分。
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
15年8月12日在13:34

\ $ \ begingroup \ $
@AaronBertrand将conString移到SqlConnection的构造函数将不会连接到数据库。它仅节省您在将该变量重置为完全相同的内容100,000次时所浪费的资源。
\ $ \ endgroup \ $
– Der Kommissar
15年8月12日在13:37

\ $ \ begingroup \ $
对于连接字符串,实际上使用了不同的连接,我只是略作简化。对于console.writeline东西,您可以假定与数据交互的实际应用程序实际上可以完成工作。由于在所有情况下向控制台写入日期时间的成本都应该相同,因此是否要在所有测试中始终包含额外的时间,或者从未在任何测试中包含额外的时间,这真的很重要吗?我正在尝试衡量相对表现,而不是绝对。
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
15年8月12日在13:49

\ $ \ begingroup \ $
您应该发布与实际代码最接近的代码,否则您将获得与实际代码不符的评论。
\ $ \ endgroup \ $
–马拉奇♦
15年8月12日在13:51

\ $ \ begingroup \ $
安顿下来,我只是合并了两个单独的conString变量。
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
15年8月12日在14:15

#4 楼


for循环

您可以使用while循环,而不是使用i循环,并且具有迭代器变量for。一个简单的for循环的结构如下:

for(/* Declare `i` */; /* Check the value of `i` against a condition */; /* Change the value of `i` */)
{
    ...
}


例如,此while循环在代码开头附近:

int i = 1;
while (i <= 100000)
{
    ...
}


将成为以下for循环

for(int i = 1; i <= 100000; ++i)
{

}


通过使用for循环,您可以无需声明i并对其进行递增


重复代码的分离

我注意到您将这一行代码重复了三遍:

Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));


虽然在像这样的小程序中,这没什么大不了的,但我还是建议将其封装在如下的辅助方法中:

private void CurrentUtcDate()
{
    Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff"));
}


然后您可以将此方法添加到Program类中,并调用this.CurrentUtcDate()CurrentUtcDate()以达到相同的效果。


连帽衫

我看不到需要我挑剔的与此代码相关的许多事情,但是以下几项可以更改:


添加s Main方法中的一些空白行。这将有助于提高可读性。
还要在其中插入一些内联注释//,解释某些代码块的功能以及它们的工作方式。
删除代码顶部无用的using语句,就像System.Collections.GenericSystem.Text一样。


评论


\ $ \ begingroup \ $
如果删除了System.Configuration,则为IIRC,我无法从App.Config中提取ConfigurationManager.ConnectionStrings。同时->是否需要进行有意义的更改(考虑到我的大多数基于SQL Server的读者比while更加熟悉),还是更多地进行了微优化?
\ $ \ endgroup \ $
–亚伦·伯特兰(Aaron Bertrand)
2015年8月12日在13:44



\ $ \ begingroup \ $
CurrentUtcDate()的描述性不是很好,并且具有意外的副作用。我会使用类似PrintCurrentUtcDate()或PrintDate(DateTime date)的方法,然后调用PrintDate(DateTime.UtcNow)。
\ $ \ endgroup \ $
– Dorus
15年8月13日在12:36