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

时间:2013-05-17 15:01:17

标签: c# .net performance profiler zedgraph

我有一个相对较大的系统(到目前为止约25000行)用于监控无线电相关设备。它使用最新版本的ZedGraph显示图表等。 该程序使用Win7上的VS2010上的C#进行编码。 问题是:

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

我希望程序能够快速运行!

解决方案中的每个项目都设置为RELEASE,Debug非托管代码是DISABLED,定义DEBUG和TRACE常量是DISABLED,优化代码 - 我试过,警告级别 - 我试过,抑制JIT - 我试过, 总之,我尝试了StackOverflow上已经提出的所有解决方案 - 没有一个工作。程序在分析器外部很慢,在分析器中很快。 我不认为问题出在我的代码中,因为如果我将探查器附加到其他不相关的进程中它会变得很快!

请帮忙! 我真的需要它在所有地方都那么快,因为它是一个关键业务应用程序,性能问题是不能容忍的......

更新1 - 8跟随

-------------------- UPDATE1:--------------------

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

-------------------- UPDATE2:--------------------

在虚拟机中运行程序,程序仍然运行缓慢,并且从主机运行的探查器不会使它快速。

-------------------- UPDATE3:--------------------

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

-------------------- UPDATE4:--------------------

如果我打开英特尔图形驱动程序设置窗口(此事:http://www.intel.com/support/graphics/sb/img/resolution_new.jpg) 并且只是不停地用光标悬停在按钮上,这样它们会发光,等等,我的程序加速了! 如果我运行GPUz或Kombustor,它不会加速,所以没有GPU上的超频 - 它保持稳定850Mhz。

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

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

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

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

- 在配备专用Radeon X1650的Core i5-2400S上,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();
    }

-------------------- Update7:--------------------

通过ANTS探查器运行它。它告诉我,当程序快速时ZedGraph刷新计数精确比慢速时高两倍。 以下是截图: screenshot of ANTS when slow screenshot of ANTS when fast

我觉得非常奇怪,考虑到各部分长度的微小差异,性能与数学精度相差两次。

另外,我更新了GPU驱动程序,但没有帮助。

-------------------- Update8:--------------------

不幸的是,几天之后,我无法重现这个问题......我的速度一直是可接受的(这仍然比我两周前在探查器中的速度慢一点)受两周前曾影响它的任何因素影响 - 分析器,视频捕获或GPU驱动程序窗口。我仍然没有解释导致它的原因......

5 个答案:

答案 0 :(得分: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;
}

调用Application.DoEvents()可能会在GUI内容未被禁用且用户同时第二次启动其他事件或相同事件时引发一系列令人头疼的问题,导致堆栈爬升,其本质排队后面的第一个操作新的,但我要离题了。

可能这个例子太具体了,我会尝试做一个更一般的例子。如果你的线程正在填充由其他线程处理的缓冲区,请确保在循环中留下一些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()置于正确的位置可以帮助改善其他线程的流量

----------在Update7之后编辑-------------

我完全删除了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%)。

你没有看到另一个rootnode的原因是因为第3个下拉框选择了“[604] Mian Thread”,我之前没有注意到。

至于更流畅的图表,在仔细观察屏幕截图后,我现在对此有了第二个想法。主线程显然已收到更多(双重)更新消息,并且CPU仍有一些空间。

看起来线程在不同时间不同步并且同步,更新消息到达太晚(当WndProc完成并进入休眠状态一段时间),然后突然及时一会儿。我对蚂蚁不是很熟悉,但是它有一个并排的线程时间表,包括睡眠时间吗?你应该能够看到这种观点中发生了什么。微软threads view tool会派上用场: enter image description here

答案 1 :(得分:3)

Luaan在上面的评论中发布了解决方案,它是系统范围的计时器分辨率。默认分辨率为15.6 ms,探查器将分辨率设置为1ms。

我遇到了完全相同的问题,执行速度非常慢,在打开探查器时速度会加快。问题在我的电脑上消失了,但似乎随机地重新出现在其他电脑上。我们还注意到在Chrome中运行Join Me窗口时问题消失了。

我的应用程序通过CAN总线传输文件。该应用程序加载带有8个字节数据的CAN消息,发送它并等待确认。定时器设置为15.6ms,每次往返只需15.6ms,整个文件传输大约需要14分钟。定时器设置为1ms,往返时间不同但会低至4ms,整个传输时间将减少到不到两分钟。

您可以通过以管理员身份打开命令提示符并输入以下内容来验证系统计时器分辨率以及找出哪个程序提高了分辨率:

powercfg -energy duration 5

输出文件将在其中包含以下内容:

平台计时器分辨率:平台计时器分辨率 默认平台计时器分辨率为15.6ms(15625000ns),应在系统空闲时使用。如果定时器分辨率增加,则处理器电源管理技术可能无效。由于多媒体回放或图形动画,可以增加定时器分辨率。 电流定时器分辨率(100ns单位)10000 最大定时器周期(100ns单位)156001

我当前的分辨率为1 ms(10,000单位为100nS),后面是请求提高分辨率的程序列表。

此信息以及更多详细信息可在此处找到: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()的参数匹配。

答案 2 :(得分:0)

当我从未听过或看到类似的东西时;我建议使用常识方法来注释代码部分/在函数顶部注入返回,直到找到产生副作用的逻辑。你知道你的代码,可能有一个有根据的猜测从哪里开始砍。其他大部分都是作为一个完整性测试并开始添加块。我常常惊讶于人们能够找到那些看似不可能跟踪的错误。找到相关代码后,您将获得更多解决问题的线索。

答案 3 :(得分:0)

有很多潜在原因。在不说明完整性的情况下,您可以按照以下方法搜索实际原因:

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

  • 处理器频率。这在笔记本电脑上很容易发生。潜在地,节能系统将处理器的频率设置为较低的值以节省能量。某些应用可能会“唤醒”系统,从而增加了频率。通过性能监视器(常规)进行检查。

  • 如果应用程序运行速度比可能的慢,那么一定是资源利用效率低下。使用探查器对此进行调查!您可以将探查器附加到(缓慢的)运行进程中,以查看哪些资源未充分利用/过度利用。通常,执行速度太慢的原因有两大类:内存绑定和计算绑定执行。两者都可以使您更深入地了解导致减速的原因。

但是,如果您的应用程序实际上通过附加到探查器而改变了效率,您仍然可以使用自己喜欢的监视器应用程序查看哪些性能指标确实发生了变化。同样,perfmon是您的朋友。

答案 4 :(得分:-1)

如果您的方法抛出很多异常,则它可以在调试模式下缓慢运行,而在CPU分析模式下快速运行。

详细的here,可以使用 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;
}