提高 WPF 应用程序性能的最佳方法

Posted

技术标签:

【中文标题】提高 WPF 应用程序性能的最佳方法【英文标题】:Best way to improve performance in WPF application 【发布时间】:2019-06-14 22:49:43 【问题描述】:

我目前正在开发一个 WPF 应用程序,该应用程序是使用实体框架构建的以访问数据(SQL Server 数据库)(数据库优先)。

过去,数据库位于内部服务器上,即使数据库的实现非常糟糕(只有表、没有视图、没有索引或存储过程),我也没有注意到应用程序的性能有任何问题。我是创建它的人,但这是我的第一份工作,我对数据库不是很好,所以我觉得实体框架是主要关注代码的最佳方法。

但是,数据库现在位于另一台速度较慢的服务器上。正如您猜到的那样,该应用程序现在存在严重的性能问题(加载十几行需要超过 10 秒,插入新行也是如此......)。

我是否应该继续使用实体框架,但尝试通过更改数据库添加视图和存储过程来提高性能? 我应该摆脱实体框架,只使用“基本”代码(同时改进数据库)吗? 有没有可以用来代替 EF 的简单 ORM?

时间在这里不是问题,我可以一直使用我想改进应用程序的时间,但我似乎无法决定改进我的应用程序的最佳方式。

数据库非常简单(大约 10 个表),唯一可能使事情复杂的是我在其中存储文件。所以我不确定我真的可以使用我想要的任何东西。而且我不知道它是否重要,但我需要显示很多计算字段。 有什么建议吗?

请随时提出任何相关问题。

【问题讨论】:

您查明了性能不佳的原因吗?我很难相信数据库是原因(数据库速度非常快,即使是查询的数万个结果)。您正在读取的数据有多大?如果它太大,则可能是网络带宽问题,或者问题可能在于在您的应用程序中处理它需要很长时间 修复服务器,我怀疑你是否能解决网络/服务器性能问题 如果您已经知道您的数据库是问题所在,为什么您认为 any 其他 ORM 会表现更好?您应该更改导致问题的图层,而不是更改那些可扩展的图层。 @MindSwipe 有很多原因导致即使是最快的数据库也可能表现不佳。 dbs 本身速度很快是不正确的。 很明显,与服务器的连接是问题所在,因为当服务器更改时行为发生了变化,但是您可以想象我无法触摸它,这不是我能控制的。 【参考方案1】:

对于性能分析,我建议首先查看的是 SQL 分析器。这可以捕获 EF 正在运行的确切 SQL 语句,并帮助识别可能的性能罪魁祸首。我介绍了其中的一些here。架构问题可能是最相关的起点。标题以 MVC 为目标,但大多数项目与 WPF 和任何应用程序有关。

我用于 SQL Server 的一个好的、简单的分析器是 ExpressProfiler。 (https://github.com/OleksiiKovalov/expressprofiler)

随着迁移到新服务器,它现在通过网络发送数据而不是从本地数据库中提取数据,您注意到的性能问题很可能属于“加载过多”的类别经常”。现在您不仅要等待数据库加载数据,还要等待它打包并通过网络发送。此外,新数据库是否代表相同的数据量并且只为单个客户端提供服务,还是现在为多个客户端提供服务?开发人员的其他问题是“在我的机器上工作”,其中本地测试数据库较小并且不处理来自服务器的并发查询。 (锁等会影响性能)

从这里开始,使用隔离的数据库服务器(没有其他客户端访问它以减少“噪音”)运行应用程序的副本,并在其上运行分析器。需要注意的事项:

延迟加载 - 在这种情况下,您有查询来加载数据,但随后会看到大量(数十到数百)个附加查询被剥离。您的代码可能会说“运行此查询并填充此数据”,您期望它应该是 1 个 SQL 查询,但是通过触摸延迟加载的属性,这可以衍生出许多其他查询。 延迟加载的解决方案:如果您需要额外的数据,请使用.Include() 急切加载。如果您只需要一些数据,请考虑使用.Select() 来选择您需要的数据的视图模型/DTO,而不是依赖完整的实体。这将消除延迟加载场景,但可能需要对代码进行一些重大更改才能使用视图模型/dto。像 Automapper 这样的工具在这里可以提供很大帮助。阅读 .ProjectTo() 以了解 Automapper 如何与 IQueryable 一起工作以消除延迟加载命中。

阅读过多 - 加载实体可能会很昂贵,尤其是在您不需要所有这些数据的情况下。性能的罪魁祸首包括过度使用.ToList(),这将实现需要数据子集的整个实体集,或者简单的存在检查或计数就足够了。例如,我见过这样的代码:

var data = context.MyObjects.SingleOrDefault(x => x.IsActive && x.Id = someId);
return (data != null);

这应该是:

var isData = context.MyObjects.Where(x => x.IsActive && x.Id = someId).Any();
return isData;

两者之间的区别在于,在第一个示例中,EF 将有效地执行 SELECT * 操作,因此在存在数据的情况下,它会将所有列返回到实体中,仅稍后检查实体是否展示。第二条语句将运行一个更快的查询来简单地返回一行是否存在。

var myDtos = context.MoyObjects.Where(x => x.IsActive && x.ParentId == parentId)
  .ToList()
  .Select( x => new ObjectDto
  
    Id = x.Id,
    Name = x.FirstName + " " + x.LastName,
    Balance = calculateBalance(x.OrderItems.ToList()),
    Children = x.Children.ToList()
      .Select( c => new ChildDto  
      
        Id = c.Id,
        Name = c.Name
      ).ToList()
  ).ToList();

这样的语句可以继续执行并且变得相当复杂,但真正的问题是 .Select() 之前的 .ToList()。由于开发人员试图做一些 EF 不理解的事情,例如调用方法,这些通常会悄悄出现。 (即calculateBalance()),它通过首先调用.ToList()“工作”。这里的问题是,此时您正在具体化整个实体并切换到 Linq2Object。这意味着对相关数据(例如 .Children)的任何“触摸”现在都将触发延迟加载,并且进一步的 .ToList() 调用可以使更多数据饱和到内存中,否则这些数据可能会在查询中减少。要注意的罪魁祸首是.ToList() 呼叫并尝试删除它们。在调用 .ToList() 之前选择更简单的值,然后将该数据输入到视图模型中,视图模型可以计算结果数据。

我见过的最糟糕的罪魁祸首是开发人员想在 Where 子句中使用函数:

var data = context.MyObjects.ToList().Where(x => calculateBalance(x) > 0).ToList();

第一个ToList() 语句将尝试使整个表饱和到内存中的实体。除了加载所有这些数据所需的时间/内存/带宽之外,一个巨大的性能影响仅仅是数据库必须进行的锁定数以可靠地读取/写入数据。您“触摸”的行越少,触摸它们的时间越短,您的查询就可以更好地处理来自多个客户端的并发操作。随着系统过渡到被更多用户使用,这些问题会大大放大。

如果您已经消除了额外的延迟加载和不必要的查询,接下来要看的是查询性能。对于看起来很慢的操作,将 SQL 语句从分析器中复制出来并在数据库中运行,同时查看执行计划。这可以提供有关可以添加以加快查询速度的索引的提示。同样,使用.Select() 可以通过更有效地使用索引并减少服务器需要拉回的数据量来大大提高查询性能。

对于文件存储:这些是作为列存储在相关表中还是在链接到相关记录的单独表中?我的意思是,如果您有发票记录,并且还有保存在数据库中的发票文件的副本,是不是:

发票

发票编号 发票编号 ... 发票文件数据

发票

发票编号 发票编号 ...

发票文件

发票编号 发票文件数据

将大的、很少使用的数据保存在单独的表中而不是与常用数据组合是一种更好的结构。这使得加载实体的查询小而快,可以在需要时按需提取昂贵的数据。

如果您使用 GUID 作为键(而不是整数/长整数),您是否在利用 newsequentialid()? (假设 SQL Server)键设置为使用 newid() 或在代码中,Guid.New() 将导致索引碎片和性能不佳。如果您通过数据库默认值填充 ID,请将它们切换为使用 newsequentialid() 来帮助减少碎片。如果您通过代码填充 ID,请查看编写一个模仿 newsequentialid() (SQL Server) 或适合您的数据库的模式的 Guid 生成器。 SQL Server 与 Oracle 存储/索引 GUID 值不同,因此将 UUID 字节的“类静态”部分置于数据的高阶与低阶字节将有助于索引性能。还要考虑索引维护和其他数据库维护工作,以帮助保持数据库服务器高效运行。

谈到索引调优,数据库服务器报告是您的朋友。在您从代码中消除了大部分,或者至少是一些严重的性能违规者之后,接下来就是查看您的系统的实际使用情况。了解代码/索引调查目标的最佳方法是数据库服务器识别的最常用和问题查询。在这些是 EF 查询的情况下,您通常可以根据 EF 查询负责的命中表进行逆向工程。获取这些查询并通过执行计划提供它们,以查看是否有可能有帮助的索引。索引是开发人员要么忘记,要么过早关注的事情。太多的索引可能和太少一样糟糕。我发现最好在决定添加哪些索引之前监控实际使用情况。

这有望让您开始寻找要寻找的东西,并将该系统的速度提高一个档次。 :)

【讨论】:

【参考方案2】:

首先你需要运行一个性能分析器,发现瓶颈是什么,可以是数据库、实体框架配置、实体框架查询等等

根据我的经验,实体框架是此类应用程序的不错选择,但您需要了解它的工作原理。

另外,您使用的是什么实体框架?最新版本是 6.2 并且有一些老年人没有的性能改进,所以如果您使用的是旧版本,我建议更新它

【讨论】:

我已经运行了一个性能分析器,但说实话,如果你是第一次使用它很难分析它,我不知道在哪里看! 是的,您应该更新。我建议多学习一下性能分析器,有内存分析器、cpu 分析器等等。您需要检查代码中的哪些进程需要很长时间才能执行。从这个视频开始:youtube.com/watch?v=LhhGqNAGQHU【参考方案3】:

根据 cmets,我猜测这主要是带宽问题。

您有一个应用程序在它位于同一位置时运行良好,可能是单个交换机、千兆以太网和 200 米的电缆。

现在,该应用程序正试图通过公共互联网通过未知数量的内部代理与谁知道其他流量发生争用,从而向远程服务器发送或检索数据,并且它的性能不佳。

您还提到您将文件存储在数据库中,并且您的架构具有Attachment.dataDoc.file_content 之类的字段。这表明您可能会尝试为简单的查询传输大量(可能是兆字节)数据,而这正是您失败的地方。

一些通用指针:

为要连接表或值的任何位置添加索引 常查询。 注意实体框架中Lazy & Eager loading 之间的区别。没有正确或错误的答案, 但你应该知道你正在使用什么方法以及为什么。 拆分任何文件内容 放入自己的表中,与主表具有相同的主键 使用不同的 EF 类以确保您只检索文件 当您需要使用它们时。

【讨论】:

以上是关于提高 WPF 应用程序性能的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

WPF应用程序的性能提升

WPF:换出控制模板可以提高性能和效率?

在 WPF 应用程序中本地保存数据的最佳方法是啥?

本地化 WPF 应用程序的最佳方法是啥,没有 LocBAML?

遵循 MVVM 模式在 WPF 应用程序中处理导航的最佳方法是啥?

在 WPF 中的应用程序中创建可换肤图像的最佳方法