红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

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:052864
用户头像

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

关注

评论

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

路由基础:三层交换机、单臂路由的特点以及配置特点、DHCP报文类型、DHCP工作原理、在路由器上配置DHCP、在交换机上配置DHCP、配置DNS服务器

Python-派大星

10月月更

线性表数据结构和使用数组实现顺序表

石柒

10月月更

老生常谈React的diff算法原理-面试版

beifeng1996

React

鸿蒙开发实例 | 为什么选择HarmonyOS?

TiAmo

华为 鸿蒙 10月月更

offset新探索:双管齐下,加速大数据量查询

华为云开发者联盟

数据库 后端 华为云 企业号十月 PK 榜

OSPF基础(一):OSPF工作原理以及优点、OSPF的缺点、OSPF配置方法、DR与BDR的选举原理

Python-派大星

10月月更

实现企业转型,华为云大数据实力领先

IT科技苏辞

ARM架构下的Docker环境,OpenJDK官方没有8版本镜像,如何完美解决?

程序员欣宸

Java Docker 10月月更

JavaScript数组常用方法大全

CoderBin

JavaScript 面试 前端 数组 10月月更

vue项目启动失败,内存不足处理

甜点cc

JavaScript Vue V8 10月月更

前端安全性问题以及防御措施

甜点cc

前端 安全 学习笔记 10月月更

假如面试官问你Babel的原理该怎么回答

loveX001

JavaScript

从React源码分析看useEffect

goClient1992

React

Kafka生产者客户端几种异常Case详解

石臻臻的杂货铺

kafka Kafka实战 10月月更

Tapdata 创始人唐建法:以秒级响应速度,为企业提供实时数据服务 | 阿里云云原生加速器特别报道

阿里巴巴云原生

阿里云 云原生 Tapdata

“程”风破浪的开发者|GoFrame 如何优雅的共享变量?

王中阳Go

Go golang 学习方法 10月月更 “程”风破浪的开发者

熬夜整理前端高频面试题(已拿offer)

loveX001

JavaScript

华为云大数据,共建智能世界的数据底座

IT科技苏辞

第九期-模块三

wuli洋

【一Go到底】第二十七天---数组注意事项及细节

指剑

Go 10月月更 goalgn

前端一面高频react面试题(持续更新中)

beifeng1996

React

React源码分析8-状态更新的优先级机制

goClient1992

React

如何快速解决恶意进程导致CPU飙升问题 | Linux | 运维

Appleex

Linux centos 运维 cpu 服务器

使用NeuVector开展云原生安全测试

QE_LAB

云原生 安全测试

项目开发全流程梳理

甜点cc

团队管理 技术管理 敏捷研发 10月月更

IP路由基础、路由器静态路由配置方法、自治系统、缺省路由的配置方法、路由选路规则、缺省路由、备份路由、等价路由、三种查询路由表命令

Python-派大星

10月月更

云服务的智能堪称颠覆,华为云带来多种守护

IT科技苏辞

用户故事地图怎么用?实践才能出真知

敏捷开发

敏捷 敏捷开发 用户故事 用户故事地图

String源码分析(一)

知识浅谈

10月月更

面试官:聊聊长连接下的负载均衡

小小怪下士

Java 负载均衡 程序员 TCP

问:React的setState为什么是异步的?

beifeng1996

React

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