QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

C++/CX 性能陷阱

  • 2013-10-30
  • 本文字数:3213 字

    阅读完需:约 11 分钟

使用 C++/CX 编写应用程序和编写正常的 C++ 应用程序不一样。纯 C++ 代码和 Windows 运行时(WinRT)之间的互操作性出奇的昂贵。基于 Sridhar Madhugiri 的视频 C++/CX 最佳实战中的内容,我们在本文中列举了一些在 Windows 8 开发中避免性能问题的方式。

边界

在应用程序的边界上会产生多种性能障碍。

数据转换就是其中的一个例子。考虑一下一个 Web 服务客户端和应用程序剩余部分之间的典型边界。大多数 Web 服务是使用 UTF-8 编码的,而大多数 Windows 应用程序的内部则是使用 UTF-16 编码的。在 Windows 中 UTF-16 编码是如此的流行以致于人们有时会将它错误地称为“Unicode”编码。数据转换的成本可能是确定的,也可能广泛变化,这依赖于它在数据本身中的特定值。

下一种性能消耗来自于类型转换。例如,你可能需要一个 wstring,但是却有一个 wchar_t *。尽管在内存中每种类型所包含的数据看起来是一样的,但是将这些内容从一个数据结构复制到另一个数据结构依然是有性能成本的。

最后一种性能消耗来自于数据复制操作。有时候你必须为边界处的数据复制付出代价,哪怕它们并不需要数据转换和类型转换。

我们为什么要在现在讨论这些内容呢?原因是 WinRT 本身就是应用程序和操作系统其余部分之间的边界。编写高性能 C++/CX 应用程序的本质就是识别边界并在可能的情况下避免跨越边界。

如果跨越 WinRT 边界的操作无法避免,那么就寻找一些方式减少数据复制、类型转换和数据转换操作的数量。例如,如果数据源和目标都使用 UTF-8 编码,那么就没必要将数据转换为 UTF-16,因为你最终还是需要将其再转换回来。

字符串

在大多数应用程序中字符串都是主要的数据类型。文件系统、Web 服务、UI、消息、符文和契约等领域对字符串的依赖性日益加深。不幸的是人们所使用的字符串类型非常多。

在内部,大多数应用程序可能会使用 std::wstring 或者 std::wchar_t*,你所依赖的大多数第三方类库也是如此。但是在与 WinRT 类库进行通信的时候你需要切换到 Platform::String^ 。每一次转换都需要一次内存分配和一次数据复制操作。

String^ 和本地 C++ 版本之间的一个关键区别是:String^ 是不可变的。WinRT 运行时对不可变字符串的这种强调可能来自于.NET 和 CLR。正如 ^ 符号所表示的,String^ 也是引用计数。

人们可能会对可变和不可变字符串相关的优点争论一整天,但是最终只有一个事实。因为 C++ 标准类库只理解可变字符串,而 WinRT 仅理解不可变字符串,所以对这两者你都必须进行处理。正如前面所提到的,这意味着需要对字符串进行复制。

类库作者:如果你正在构建一个一般用途的类库供他人使用,那么你应该考虑提供多个不同版本的 API,为每种字符串类型提供一个 API。这样你就不需要猜测 API 的使用者在调用类库的时候使用的是哪种字符串类型了。

很多基于字符串的操作实际上并不需要使用字符串,但是开发者宁可选择使用字符串迭代器。因为可变和不可变数据结构的迭代操作是一样的,你可以在使用常规 xxx_iterator( begin(string), end(string), …) 语法的字符串平台上直接创建 STL 样式的迭代器。

另外,首先要查找直接返回 wchar_t* 的 API,而不是将它封装成一个 wstring。如果你找到了这样的 API,那么你就能够通过数组中第一个元素的地址以及数组的长度创建一个新的 platform string。这样就不需要创建一个在匹配的 platform string 被创建之后立即就会被废弃的 wstring。

调用带有字符串引用( StringReference )类型输入参数的 WinRT API 时有一个小窍门。你可以向一个参数类型为 platform string 的 WinRT 函数传递一个 wchar_t* 或者 wstring 参数,这种情况下将创建一个轻量级外观。无论如何,这里有一些需要注意的地方。

  1. 字符串必须是空终止否则将会抛出一个错误。
  2. 如果字符串在函数之外的任何地方发生了变化,那么结果将无法确定。
  3. 如果函数之内有任何字符串的引用,那么无论如何都会生成一个完整副本。

上面的第 1 条内容很容易验证,第 2 条则仅会在碰见线程安全问题的时候发生。在大多数环境下这应该是一个非常有用的技巧。

类库作者:为了确保上面的方案是真实可能的,首先尽量避免让它引用你以 StringReference 参数的方式获取到的字符串。因为随后的引用并不会引入额外的复制,所以不要担心使用第二个引用。

集合

与 C++ 中常见的集合相比,WinRT 中的集合是非常昂贵的。和.NET 中可观察的集合一样,对 WinRT 集合的每一次修改都会产生一个通知。该通知主要用于 XAML 数据绑定以便于更新 UI。

在初始化期间避免这种损失的一种方式是,首先在堆栈上创建并填充一个标准的 vector,然后使用 move 函数初始化一个 platform vector。你能够这样做,因为标准的 vector 将会被销毁,同时它的动态内存无论如何都会被释放。

在更新很多元素的时候,考虑使用 ReplaceAll 方法。这仅会触发一个通知而不是每一条记录一个通知。在 WPF 和 Silverlight 中没有与之相对应的方法,因为这些 UI 堆栈本身不支持一次性插入或者移除多个条目。

WinRT 集合中的另一种性能消耗来自于元素的读取。WinRT 集合是以接口的形式暴露的,因此它们是虚的,这就意味着它们并不能像普通的函数那样被内联。此外,每一次读取都需要进行范围检查。所以如果你需要多次读取同一个值,考虑将它复制到一个局部变量中,不要每次都从集合中读取。实际上复制的缺点是,你必须复制值或者增加对象上的引用数,这是一个连锁操作。

完全避免这种消耗的一种方式是在迭代集合之前复制它。分配一个正确大小的局部 vector,然后在 ArrayReference 上使用 GetMany 函数。然后结合使用 ReplaceAll 方法,你就能够对集合进行几次迭代,仅需要跨越 WinRT 边界三次就能够做一系列复杂的修改。

WinRT 接口

和传统的 COM 一样,在 WinRT 中一个对象的成员仅会通过接口暴露。你永远都不可能直接访问对象。C++/CX 通过做必要的隐式转换对你隐藏了这些细节。这样做之所以必要的一个常见原因是,可以满足调用非默认接口上的方法时的需要。

WinRT 中的转换不是廉价的。它需要调用 QueryInterface 这个虚方法,同时有一个增加引用数的连锁操作。一旦完成了对非默认接口的调用,还需要另一个减少引用数的连锁操作。

类库作者:

确保类中所有的常用方法在默认接口上都是可用的,这样就不需要转换成另一个接口了。

如果要对同一个非默认接口进行多次调用,那么创建一个该接口类型的局部变量。这样仅需要执行一次转换,而不是每次方法调用时都做一次转换。

在任何可能的时候你都应该使用堆栈分配或者 unique_ptr 类。因为这样你将获得所有选项的最好性能。

在你确实需要一个复杂生命周期的时候,你的下一个选择是通过一个 shared_ptr 访问的普通 C++ 类。这种方式和上面选项之间的主要区别在于引用数开销。

你选择的最后一种手段应该是 ref 类。一个 ref 类拥有和 shared_ptr 相似的引用数语义,但是能够带来其他基于 WinRT 的开销。所以仅在需要将类传递到一个 WinRT 函数或者在 XAML 的数据绑定中使用 ref 类。

在使用 ref 类的时候,尽量保持较浅的继承层次。WinRT 继承和 C++ 继承不一样,它有额外的开销。

XAML 数据绑定

在 WinRT+XAML 数据绑定中你应该避免实现 INotifyPropertyChanged,除非你确实希望在属性被填充之后发生改变。同样的,不要暴露公共 set 函数,除非 UI 确实需要修改数据。

XAML 所调用的 get 函数应该是廉价的。不仅仅是因为它们是在 UI 线程上被调用的,还因为我们可能会调用它们多次。所以不要在 get 函数中分配内存或者执行昂贵的计算。

对于所有基于 XAML 的 UI(WPF、Silverlight、WinRT+XAML)而言,一点非常重要的建议是保持较浅的数据层次。绑定表达式中的每一个点都代表了一次属性改变事件,而为了保持屏幕及时更新数据绑定引擎必须监听这些事件。

关于作者

Jonathan Allen 从 2006 年开始就一直在为 InfoQ 编写新闻,现在是.NET 版块的首席编辑。如果你有兴趣为 InfoQ 撰写新闻或者教育性的文章,那么请联系他:jonathan@infoq.com.

查看英文原文 C++/CX Performance Pitfalls

2013-10-30 23:053277
用户头像

发布了 321 篇内容, 共 123.2 次阅读, 收获喜欢 19 次。

关注

评论

发布
暂无评论
发现更多内容

阿里云智能编码插件,更Cosy的开发体验

阿里云云效

Java 阿里云 程序员 开发 研发

数据可视化大屏 - 大屏云极简使用手册

shulinwu

可视化 数据可视化 大屏可视化 智慧大屏可视化 大屏

低代码平台设计探索,如何更好赋能开发者

雯雯写代码

低代码 开发平台 开发者,

HertzBeat赫兹跳动v1.0.beta.4 发布, 易用友好的高性能监控告警系统

TanCloud探云

开源 APM 监控 监控系统 监控告警

Nebula Graph 的 KV 存储分离原理和性能测评

NebulaGraph

图数据库 分布式图数据库

ironSource 新功能发布,开发者可在同一会话中实时调整广告策略

极客天地

实践GoF的23种设计模式:SOLID原则(上)

华为云开发者联盟

设计模式 GoF SOLID SOLID原则 分布式应用系统

墨天轮国产数据库沙龙 | 许力:阿里云原生Lindorm TSDB数据库,驱动工业IT&OT超融合数字化系统升级

墨天轮

数据库 阿里云 tsdb

应用数仓ODBC前,这些问题你需要先了解一下

华为云开发者联盟

数据库 GaussDB(DWS) 驱动 ODBC 驱动管理器

教你如何使用flask实现ajax数据入库

华为云开发者联盟

Python 数据库 flask 文件上传 ajax数据

星环科技ArgoDB 3.2正式发布,全面升级易用性、性能和安全

星环科技

数据库

新思科技最新报告显示97%的应用存在漏洞

InfoQ_434670063458

新思科技 应用安全

创建公司内部文档的入门指南

小炮

工作效率 企业管理 企业管理软件

【BBC learningenglish】with Tango

IT蜗壳-Tango

IT蜗壳教学 3月月更 Tango English

赋能金融领域,国密改造让安全合规更加牢固

电子信息发烧客

安全

易观分析对《关于银行业保险业数字化转型的指导意见》的解读

易观分析

银行 数字化

白话大数据 | 从买菜这件小事来聊聊数据仓库

星环科技

前端架构三大巨头之一Angular | 深度讲解

云智慧AIOps社区

开源 前端 Web angular 数据源

一文带你看懂HarmonyOS应用上架

HarmonyOS开发者

HarmonyOS 应用开发

【案例】基于星环科技数据云平台TDC为富国基金建设万能的数据湖

星环科技

数据库

BFS/DFS/DP 算法案例 LeetCode题目:传递信息

OpenHacker

LeetCode 动态规划 深度优先搜索 算法解析 广度优先搜素

Web 键盘输入法应用开发指南 (2) —— 键盘事件

天择

JavaScript 键盘 输入法 3月月更

【C语言】数据类型存储、原码,反码,补码

謓泽

C语言 补码 原码 反码 3月月更

用 python selenium 爬简书,Python自动化领域之 Selenium WebDriver 学习第2篇

梦想橡皮擦

Python 3月月更

布局说明 - 大屏云极简使用手册

shulinwu

可视化 数据可视化 大屏可视化 智慧大屏可视化 大屏

活动预告 | DataOps + MLOps Meetup

第四范式开发者社区

人工智能 机器学习 开源 DevOps MLOps

2021 “科创中国”开源创新榜单公布,优麒麟荣登两榜!

优麒麟

Linux 开源 开源社区 优麒麟

融合通信常见问题2月刊 | 云信小课堂

网易云信

音视频 融合通信

开讲了!龙蜥社区走进北大课堂

OpenAnolis小助手

开源 操作系统 龙蜥社区 北京大学 走进高校

虎符研究院关于 Manta Network平行链解决方案的分享

区块链前沿News

虎符交易所 币圈后浪

组件简介 - 大屏云极简使用手册

shulinwu

数据可视化 大屏可视化 智慧大屏可视化 大屏

C++/CX性能陷阱_.NET_Jonathan Allen_InfoQ精选文章