为什么我的C#程序在性能分析器中会更快?

15

我有一个监控无线电设备的相对较大的系统(迄今为止约25000行代码)。使用最新版本的ZedGraph显示图表等。程序使用C#在VS2010和Win7上编码。

  • 当我从VS中运行程序时,它运行得很慢
  • 当我从构建的EXE中运行程序时,它运行得很慢
  • 当我通过Performance Wizard / CPU Profiler运行程序时,它运行得非常快。
  • 当我从构建的EXE中运行程序,然后启动VS并将分析器附加到任何其他进程时,我的程序会加速!

我希望程序始终保持那样快!

解决方案已经在StackOverflow上提出,所有项目都设置为RELEASE,未托管代码调试已禁用,DEFINE DEBUG和TRACE常量也已禁用,代码优化-I tried either,警告级别-I tried either,抑制JIT - I tried either。

我认为问题不在我的代码中,因为如果我附加分析器到其他不相关的进程,它也会变快!

请帮帮我!我真的需要程序在任何地方都像在分析器中一样快速运行,因为这是关键业务应用程序,不能容忍性能问题...

更新1-8如下

--------------------Update1:--------------------

问题似乎与ZedGraph无关,因为我用自己的基本绘图替换了ZedGraph后,问题仍然存在。

--------------------Update2:--------------------

在虚拟机中运行程序时,程序仍然运行缓慢,并且从主机计算机运行分析器也不能使其变快。

--------------------Update3:--------------------

启动屏幕捕获到视频中也会加速程序!

--------------------Update4:--------------------

如果我打开英特尔显卡驱动程序设置窗口(此窗口:http://www.intel.com/support/graphics/sb/img/resolution_new.jpg),然后不断将光标悬停在按钮上,使它们发光等,我的程序就会加速!。但是如果运行GPUz或Kombustor,则不会加速,因此GPU不会降频-它保持稳定的850MHz。

--------------------Update5:--------------------

在不同的机器上进行测试:

- 在我的Core i5-2400S with Intel HD2000上,UI 运行缓慢,CPU 使用率为 ~15%。

- 在同事的Core 2 Duo with Intel G41 Express上,UI 运行快,但CPU 使用率为 ~90%(这也不正常)

- 在Core i5-2400S with dedicated Radeon X1650上,UI运行非常快,CPU使用率为 ~50%。

--------------------Update6:--------------------

我的一部分代码显示如何更新单个图表(graphFFTZedGraphControl的封装,以便于使用):

public void LoopDataRefresh() //executes in a new thread
        {
            while (true)
            {
                while (!d.Connected)
                    Thread.Sleep(1000);
                if (IsDisposed)
                    return;
//... other graphs update here
                if (signalNewFFT && PanelFFT.Visible)
                {
                    signalNewFFT = false;
                    #region FFT
                    bool newRange = false;
                    if (graphFFT.MaxY != d.fftRangeYMax)
                    {
                        graphFFT.MaxY = d.fftRangeYMax;
                        newRange = true;
                    }
                    if (graphFFT.MinY != d.fftRangeYMin)
                    {
                        graphFFT.MinY = d.fftRangeYMin;
                        newRange = true;
                    }

                    List<PointF> points = new List<PointF>(2048);
                    int tempLength = 0;
                    short[] tempData = new short[2048];

                    int i = 0;

                    lock (d.fftDataLock)
                    {
                        tempLength = d.fftLength;
                        tempData = (short[])d.fftData.Clone();
                    }
                    foreach (short s in tempData)
                        points.Add(new PointF(i++, s));

                    graphFFT.SetLine("FFT", points);

                    if (newRange)
                        graphFFT.RefreshGraphComplete();
                    else if (PanelFFT.Visible)
                        graphFFT.RefreshGraph();

                    #endregion
                }
//... other graphs update here
                Thread.Sleep(5);
            }
        }

SetLine 是:

public void SetLine(String lineTitle, List<PointF> values)
    {
        IPointListEdit ip = zgcGraph.GraphPane.CurveList[lineTitle].Points as IPointListEdit;
        int tmp = Math.Min(ip.Count, values.Count);
        int i = 0;
        while(i < tmp)
        {
            if (values[i].X > peakX)
                peakX = values[i].X;
            if (values[i].Y > peakY)
                peakY = values[i].Y;
            ip[i].X = values[i].X;
            ip[i].Y = values[i].Y;
            i++;
        }
        while(ip.Count < values.Count)
        {
            if (values[i].X > peakX)
                peakX = values[i].X;
            if (values[i].Y > peakY)
                peakY = values[i].Y;
            ip.Add(values[i].X, values[i].Y);
            i++;
        }
        while(values.Count > ip.Count)
        {
            ip.RemoveAt(ip.Count - 1);
        }
    }

RefreshGraph 是:

public void RefreshGraph()
    {
        if (!explicidX && autoScrollFlag)
        {
            zgcGraph.GraphPane.XAxis.Scale.Max = Math.Max(peakX + grace.X, rangeX);
            zgcGraph.GraphPane.XAxis.Scale.Min = zgcGraph.GraphPane.XAxis.Scale.Max - rangeX;
        }
        if (!explicidY)
        {
            zgcGraph.GraphPane.YAxis.Scale.Max = Math.Max(peakY + grace.Y, maxY);
            zgcGraph.GraphPane.YAxis.Scale.Min = minY;
        }
        zgcGraph.Refresh();
    }

--------------------更新7:--------------------

我刚刚用ANTS分析器运行了它。它告诉我,当程序快速运行时,ZedGraph的刷新计数比运行缓慢时精确高出两倍。 以下是截图: 运行缓慢时ANTS的屏幕截图 运行快时ANTS的屏幕截图

我发现非常奇怪的是,考虑到这些部分长度的微小差异,性能会以数学上的精度相差两倍。

此外,我升级了GPU驱动程序,但没有帮助。

--------------------更新8:--------------------

不幸的是,几天来,我无法再复现这个问题...我得到了持续的可以接受的速度(仍然比两周前在分析器中看到的速度稍慢),并且不受以前影响它的任何因素的影响 - 分析器、视频捕获或GPU驱动程序窗口。我仍然无法解释导致这个问题发生的原因...


4
它真的有效吗?我的第一猜想是当分析器附加时,某些功能可能根本没有被执行,因此增加的速度并不相关,因为它没有发挥作用。另外,一些大概的时间会有所帮助。它是10倍加速,5%加速,还是5秒或10毫秒之间的快慢差异?此外,应用程序的可接受性能水平是多少? - Servy
@Servy - 是的,程序的每个方面都能正常工作。性能差异大约在10-50倍之间。在分析器下非常快。所需的性能水平是实时或尽可能接近实时的。接收和查看数据之间的延迟可接受高达50毫秒,但越少越好。 - Daniel
我知道分解一个复杂的应用程序很难,但请尝试进行分支并开始删除功能,直到两种模式下的行为相同。看看是否可以通过这种方式来隔离原因。 - Bobson
@Bobson - 我试过了。在我展示的10个数据图中,我禁用了9个(没有通过TCP获取它们的数据,没有运行任何解码函数,也没有尝试刷新它们的图形控件)。剩下的一个确实加速了,但在两种情况下都加速了,所以差异仍然存在。即使我禁用所有图形(使屏幕保持白色),性能仍然很差。我对大多数功能运行了计时器,所有功能最多只有0-50毫秒的延迟。然而,程序却很缓慢,UI每秒刷新一次。 - Daniel
1
也许你的代码的某些部分依赖于系统范围内的计时器分辨率?分析器可能会增加计时器频率,因此任何使用它的东西(例如等待句柄、Thread.SleepTimer等)都会更加准确。这可以解释速度提升的两种情况——例如,WPF应用程序在需要时(例如进行平滑动画时)会增加计时器分辨率,然后恢复它(当动画停止时)。分析器显然也希望有更准确的计时器。 - Luaan
显示剩余21条评论
5个回答

9
Luaan在上面的评论中发布了解决方案,它是系统范围的计时器分辨率。默认分辨率为15.6毫秒,剖析器将分辨率设置为1毫秒。
我遇到了完全相同的问题,执行非常缓慢,但当打开剖析器时会加速。这个问题在我的PC上消失了,但在其他PC上似乎随机出现。我们还注意到,在Chrome中运行Join Me窗口时,问题也会消失。
我的应用程序通过CAN总线传输文件。该应用程序加载一个带有八个字节数据的CAN消息,将其传输并等待确认。当计时器设置为15.6毫秒时,每次往返时间都需要15.6毫秒,整个文件传输大约需要14分钟。当计时器设置为1毫秒时,往返时间会有所变化,但最低可达4毫秒,整个传输时间将缩短至不到两分钟。
您可以通过以管理员身份打开命令提示符并输入以下内容来验证系统计时器分辨率以及查找增加分辨率的程序: powercfg -energy duration 5 输出文件中将包含以下内容:
平台计时器分辨率:平台计时器默认分辨率为15.6毫秒(15625000纳秒),当系统空闲时应使用。如果增加计时器分辨率,则处理器电源管理技术可能无效。由于多媒体播放或图形动画,计时器分辨率可能会增加。 当前计时器分辨率(100ns单位)10000 最大计时器周期(100ns单位)156001
我的当前分辨率为1毫秒(100ns的10000个单位),并列出了请求增加分辨率的程序列表。
此信息以及更多详细信息可以在此处找到:https://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/ 以下是一些代码,用于增加计时器分辨率(最初发布为回答此问题的答案:how to set timer resolution from C# to 1 ms?):
public static class WinApi
{
    /// <summary>TimeBeginPeriod(). See the Windows API documentation for details.</summary>

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
    [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", SetLastError = true)]

    public static extern uint TimeBeginPeriod(uint uMilliseconds);

    /// <summary>TimeEndPeriod(). See the Windows API documentation for details.</summary>

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Interoperability", "CA1401:PInvokesShouldNotBeVisible"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2118:ReviewSuppressUnmanagedCodeSecurityUsage"), SuppressUnmanagedCodeSecurity]
    [DllImport("winmm.dll", EntryPoint = "timeEndPeriod", SetLastError = true)]

    public static extern uint TimeEndPeriod(uint uMilliseconds);
}

使用以下方式来提高分辨率:WinApi.TimeBeginPeriod(1);

使用以下方式返回默认设置:WinApi.TimeEndPeriod(1);

传递给TimeEndPeriod()的参数必须与传递给TimeBeginPeriod()的参数匹配。


5

有时候减缓一个线程的速度可以显著提高其他线程的速度,通常是当一个线程经常轮询或锁定一些共同的资源。

例如(这是一个Windows窗体的例子),当主线程在紧密循环中检查总体进度而不使用计时器时:

private void SomeWork() {
  // start the worker thread here
  while(!PollDone()) {
    progressBar1.Value = PollProgress();
    Application.DoEvents(); // keep the GUI responisive
  }
}

减缓速度可以改善性能:
private void SomeWork() {
  // start the worker thread here
  while(!PollDone()) {
    progressBar1.Value = PollProgress();
    System.Threading.Thread.Sleep(300); // give the polled thread some time to work instead of responding to your poll
    Application.DoEvents(); // keep the GUI responisive
  }
}

正确的做法是尽量避免使用DoEvents调用:

private Timer tim = new Timer(){ Interval=300 };

private void SomeWork() {
  // start the worker thread here
  tim.Tick += tim_Tick;
  tim.Start();
}

private void  tim_Tick(object sender, EventArgs e){
  tim.Enabled = false; // prevent timer messages from piling up
  if(PollDone()){
    tim.Tick -= tim_Tick;
    return;
  }
  progressBar1.Value = PollProgress();
  tim.Enabled = true;
}

在GUI功能未被禁用时调用Application.DoEvents()可能会导致很多麻烦,因为用户启动其他事件或同时启动同一事件,从而导致堆栈上升,这自然会将第一个操作排队在新操作的后面,但我偏离了主题。

也许这个例子过于针对winforms,我会试着给出一个更通用的例子。如果你有一个线程正在填充由其他线程处理的缓冲区,请确保在循环中留出一些System.Threading.Thread.Sleep()余地,以允许其他线程在检查缓冲区是否需要再次填充之前进行一些处理:

public class WorkItem { 
  // populate with something usefull
}

public static object WorkItemsSyncRoot = new object();
public static Queue<WorkItem> workitems = new Queue<WorkItem>();

public void FillBuffer() {
  while(!done) {
    lock(WorkItemsSyncRoot) {
      if(workitems.Count < 30) {
        workitems.Enqueue(new WorkItem(/* load a file or something */ ));
      }
    }
  }
}

工作线程很难从队列中获取任何内容,因为填充线程不断地锁定它。在锁之外添加Sleep()可以显著加速其他线程:
public void FillBuffer() {
  while(!done) {
    lock(WorkItemsSyncRoot) {
      if(workitems.Count < 30) {
        workitems.Enqueue(new WorkItem(/* load a file or something */ ));
      }
    }
    System.Threading.Thread.Sleep(50);
  }
}

连接分析器可能会在某些情况下产生与睡眠函数相同的效果。
我不确定我是否给出了代表性的例子(很难想出简单的例子),但我想重点是清楚的,将sleep()放在正确的位置可以帮助改善其他线程的流程。
---------- 更新7后编辑 -------------
我会完全删除那个LoopDataRefresh()线程。而是在您的窗口中放置一个定时器,间隔为至少20个(如果没有被跳过,则为50帧每秒)。
private void tim_Tick(object sender, EventArgs e) {
  tim.Enabled = false; // skip frames that come while we're still drawing
  if(IsDisposed) {
    tim.Tick -= tim_Tick;
    return;
  }

  // Your code follows, I've tried to optimize it here and there, but no guarantee that it compiles or works, not tested at all

  if(signalNewFFT && PanelFFT.Visible) {
    signalNewFFT = false;

    #region FFT
    bool newRange = false;
    if(graphFFT.MaxY != d.fftRangeYMax) {
      graphFFT.MaxY = d.fftRangeYMax;
      newRange = true;
    }
    if(graphFFT.MinY != d.fftRangeYMin) {
      graphFFT.MinY = d.fftRangeYMin;
      newRange = true;
    }

    int tempLength = 0;
    short[] tempData;

    int i = 0;

    lock(d.fftDataLock) {
      tempLength = d.fftLength;
      tempData = (short[])d.fftData.Clone();
    }

    graphFFT.SetLine("FFT", tempData);

    if(newRange) graphFFT.RefreshGraphComplete();
    else if(PanelFFT.Visible) graphFFT.RefreshGraph();
    #endregion

    // End of your code

    tim.Enabled = true; // Drawing is done, allow new frames to come in.
  }
}

这是经过优化的SetLine()函数,不再需要列表形式的点数据,而是使用原始数据:

public class GraphFFT {
    public void SetLine(String lineTitle, short[] values) {
      IPointListEdit ip = zgcGraph.GraphPane.CurveList[lineTitle].Points as IPointListEdit;
      int tmp = Math.Min(ip.Count, values.Length);
      int i = 0;
      peakX = values.Length;

      while(i < tmp) {
        if(values[i] > peakY) peakY = values[i];
        ip[i].X = i;
        ip[i].Y = values[i];
        i++;
      }
      while(ip.Count < values.Count) {
        if(values[i] > peakY) peakY = values[i];
        ip.Add(i, values[i]);
        i++;
      }
      while(values.Count > ip.Count) {
        ip.RemoveAt(ip.Count - 1);
      }
    }
  }

我希望你能让它正常工作,正如我之前所说,我没有编译或检查过,因此可能会有一些错误。在那里还有更多可以优化的地方,但与跳过帧并且只在我们实际绘制下一帧之前收集数据的提升相比,优化应该是微不足道的。
如果你仔细研究iZotope视频中的图表,你会注意到它们也会跳过帧,并且有时会有些抖动。这一点一点都不糟糕,它是前台线程处理能力和后台工作者之间所做的权衡。
如果你真的想将绘图放在单独的线程中进行,则必须将图形绘制到位图上(调用Draw()并传递位图设备上下文)。然后将位图传递给主线程并进行更新。这样你就失去了IDE中设计器和属性网格的便利性,但你可以利用其他空闲的处理器核心。
---------- 对备注的编辑答案 --------
是的,有一种方法可以告诉你谁调用了什么。看看你的第一个屏幕截图,你选择了"调用树"图。每个下一行都有点跳跃(它是一个树视图,不仅仅是一个列表!)。在调用图中,每个树节点代表了由其父树节点(方法)调用的方法。
在第一张图片中,WndProc被调用了大约1800次,它处理了872个消息,其中62个触发了ZedGraphControl.OnPaint()(这又占了主线程总时间的53%)。
你没有看到另一个根节点的原因是因为第三个下拉框选择了"[604] Mian Thread",我之前没有注意到。
至于更流畅的图表,在更仔细地查看屏幕截图后,我有了第二个想法。主线程显然接收到了更多(两倍)的更新消息,而CPU仍然有一些余地。
看起来线程在不同的时间点处于不同步和同步状态,其中更新消息刚好太晚到达(当WndProc完成并暂停一段时间时),然后突然及时一段时间。我对Ants并不十分熟悉,但它是否包含一个线程时间轴,包括睡眠时间?在这样的视图中,你应该能够看到正在发生的事情。Microsoft的线程视图工具将对此非常有用:

2
通常Sleep(200)(每秒5次更新)足以被感知为实时。Sleep(33)将为您提供每秒30帧,这对于视频来说非常棒(但通常对于应用程序来说过度)。检查一下您如何更新ZedGraphs。可能您的工作线程正在等待GDI+来渲染位图,这是不必要的。您应该尽快收集数据,而不会阻塞工作线程超过必要的时间。一旦您拥有原始数据,让工作线程继续,同时您的主线程更新图表并呈现它们(在此过程中不会阻塞共享资源)。 - Louis Somers
你不应该在除了主线程之外的任何其他线程中调用Refresh()。它无论如何都会在你的主线程中运行。它没有抛出跨线程异常(实际上我期望会有),这意味着它很可能自己进行if (InvokeRequired)检查,并通过消息泵(WndProc)通过调用Invoke()将操作委托给你的主线程。坏处是它会阻塞,等待直到你的主线程处理消息并执行Refresh()函数。尝试重构,使GUI相关的内容保留在主线程中,同时保持工作线程清晰。 - Louis Somers
好的,但是如果我有5个设备窗口打开怎么办?MainThread不可能承受那么大的负载。+这不仅仅是刷新,还有在我调用ZedGraph的Refresh()之前立即发生的大量解码和缓冲。我已经更新了问题,并附上了代码片段。 此外,我尝试了一个快速编辑,其中LoopDataRefresh在主线程上执行,并定期调用Application.DoEvents(),但程序会冻结。 - Daniel
据我所记,没有其他根级节点(我现在在家,明天上班后会回复)。从我收集到的信息来看,列表中的任何内容都未经过排序或分组,并且无法确定哪个线程执行了哪个函数 - 只知道它花费了多少时间。但即使主线程进行绘图(我也相信并从未争论过),这又怎么能说明它跳帧呢?如果我可以看到更多帧(更流畅的运动)? - Daniel
啊,我明白了 :) 另外,是的,我知道并一直在使用 Concurrency Profiler 的线程视图,但不幸的是它只给我提供了一个方面的数据,因为只要有任何 VisualStudio 分析器在运行,程序总是运行得很快。所以我没有什么可以比较数据。就我所看到的,似乎没有明显的线程阻塞(没有级联执行块或重复锁定模式)。 - Daniel
显示剩余11条评论

0

当我从未听说或看到类似的东西时,我建议采用常识方法,在代码段中注释掉部分内容/在函数顶部注入返回值,直到找到产生副作用的逻辑。您了解自己的代码,并且可能有一个有根据的猜测从哪里开始切入。否则,作为一个理智的测试,大多数都要切掉,然后开始添加块。我经常惊讶于人们发现那些看似不可能跟踪的错误的速度有多快。一旦找到相关代码,您将有更多线索来解决问题。


我尝试过了,但未能隔离出任何有问题的部分。我有一种感觉,无论是什么原因导致的,它并不是我的代码中的问题。也许是关于ZedGraph的某些东西。因为如果我犯了错误,那么采取与我的运行应用程序完全无关的操作来解决它(比如将分析器附加到notepad.exe),这完全没有意义。 - Daniel
我会尝试注释掉更多的内容,即使应用程序从一个空窗口开始。这是最终的试错方法,保证产生稳定的结果,但你必须承诺注释掉所有必要的内容。 - Robert

0

存在可能导致这个问题的数组。以下是您查找实际原因的方法:

  • 环境变量:另一个答案中的定时器问题只是其中的一个例子。可能会对Path和其他变量进行修改,分析器可能会设置新的变量。将当前环境变量写入文件并比较两个配置。尝试查找可疑条目,逐个取消它们(或组合取消)直到在两种情况下都获得相同的行为。

  • 处理器频率。在笔记本电脑上很容易发生。潜在地,节能系统将处理器的频率设置为较低的值以节省能源。某些应用程序可能会“唤醒”系统,增加频率。通过性能监视器(permon)检查此问题。

  • 如果应用程序运行速度比可能的速度慢,则必然存在一些低效的资源利用。使用分析器进行调查!您可以将分析器连接到(缓慢)运行的进程以查看哪些资源被过度或不足利用。大多数情况下,执行过慢的两个主要原因是内存绑定和计算绑定。两者都可以更好地了解什么触发了减速。

然而,如果您的应用程序通过连接到分析器实际上改变了其效率,您仍然可以使用您喜爱的监视器应用程序来查看哪些性能指标实际上发生了变化。再次强调,perfmon 是您的好朋友。


-1
如果您有一个抛出很多异常的方法,在调试模式下可能会运行缓慢,而在 CPU 分析模式下则会快速运行。
此处所述,可以通过使用DebuggerNonUserCode属性来提高调试性能。例如:
[DebuggerNonUserCode]
public static bool IsArchive(string filename)
{
    bool result = false;
    try
    {
        //this calls an external library, which throws an exception if the file is not an archive
        result = ExternalLibrary.IsArchive(filename);
    }
    catch
    {

    }
    return result;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接