普通视图

发现新文章,点击刷新页面。
昨天 — 2024年7月3日技术博客

Qt and Trivial Relocation (Part 3) -- Giuseppe D'Angelo

作者 Blog Staff
2024年7月3日 06:07

kdab.pngIn the last post of this series we started exploring how to erase an element from the middle of a vector.

Qt and Trivial Relocation (Part 3)

by Giuseppe D'Angelo

From the article:

The reference semantics backstab

Let’s start by analyzing erase()‘s behavior once more.

Do you remember our claim that the specific strategy used does not really matter; that is, that they are all equivalent? Well, not so fast! It is actually quite imprecise to say that they are all equivalent.

They may be, as long as we deal with types which have value semantics. If we instead use a type that has reference semantics, the choices are absolutely not equivalent, and will yield different outcomes. This is because the semantics of assignment for (certain) reference types are write-through: they assign through the reference (instead of rebinding the reference).

Since we are implementing erasure in terms of assignments (or swaps, which boil down to assignments), this means that the precise sequence of operations done by erase will be visible due to its side-effects; and it also means that changing the strategy will produce different outcomes!

昨天以前技术博客

角色动画系统

作者 云风
2024年7月2日 14:38

Ant 引擎的角色动画系统还需要完善。

之前我们用 Ant 引擎开发的游戏以机械装置为主,所以并不需要人型角色动画。对于人物角色动作的动画控制,最好有更多的引擎支持。

通常,角色逻辑上的属性和动画表现存在一个映射关系。一个角色,它逻辑上的基本属性可能只有在空间中的坐标。我们编写代码控制它时,只关心它在哪里。但是,在做画面表现时,则需要根据空间坐标这组简单属性,转换为动画播放:如果角色静止不动,就播放 idle 动画,如果正在运动,就播放 walk 动画。

在引擎底层,每帧只通过坐标这个属性来计算角色该播放哪个动画以及怎样播放,信息是不够的。通常还需要结合过去时间线上的状态变化来推算出当前状态;或是通过几个独立的逻辑属性的组合,得到动画需要的信息。

例如,可以通过空间坐标的变化过程计算出速度;根据是否处于战斗状态决定是警戒行动还是自由行动……

这种从逻辑属性数据到表现用数据的映射关系,一般使用状态机来实现。Unity 文档对此有一个很好的描述。通过这个状态机,可以生成动画系统底层每帧需要的数据,而开发者只需要简单修改逻辑上的基本属性即可。

动画系统底层看到的是 Entity 当前状态下用于动画渲染的基础数据:一个动画片段的当前帧,或是几个动画片段的加权混合。而状态机只用运行当前帧的当前状态关联的转换逻辑去加工那些基本属性输入。

因为状态机永远处于单一状态,但对于动画来说,从一种状态到另一种状态通常有一个表现过程,所以,状态机中的状态和表现上的状态是有区别的。Unity 把两者区分开,idle ,walk 这种叫 state ,从 idle 到 walk 的过渡期叫 state transition 。从状态机的实现角度看,其实它们都是状态机的节点 node 。

对于非帧动画来说,处于 state 时,通常只有一个动画片段;在 state transition 阶段,则为它连接的两个 state 的动画片段的混合。state 可以触发特定的行为 behaviour ,开发者可以围绕每个特定的 state 来编写逻辑;而 state transition 通常由几个参数控制,对开发者是透明的。

动画状态机的 transition 和动画表现上的多个动画片段混合,是不同的两个东西。

从走路到跑步的过渡阶段可以直接切换两个做好的动画片段,如果是帧动画,动画片段有若干帧构成,通常几个可以衔接在一起的片段,每个片段的最后一帧可以和第一帧衔接在一起,保证循环播放,不同动画片段的第一帧是相同的,允许片段间切换。那么,状态机的 transition 要做的工作一般是,保持上一个状态的动画片段序列播放到最后一帧,可以顺利切换到下一个状态。

以走路过度到跑步为例,状态机在更新时,如果处于走路状态,一旦发现移动速度超过跑步的阈值,就可以切换到 walk to run 这个 transition ,在这个新的 node 中,状态机会继续保持走路的帧动画片段提供给动画底层,直到一个片段周期结束,然后切换到 run 这个新 state ,新的 state 会使用跑步的动画片段从头播放。

对于骨骼动画,往往不受单一动画片段的限制。它可以将多个动画片段混合在一起。在 transition 中,可以逐帧调整多个动画片段的混合权重。

关于人物运动怎样用多个动画片段混合,这里有一篇论文 做了非常详细的解释。

如果是简单的两组动画混合,例如运动方向不变的走路到跑步的过度,使用一维的混合即可。即逐步降低前一个动画片段的权重,同时增加目标动画的权重。

而如果要考虑人物在移动过程中转向,则需要二维的插值。通常使用 Gradient Band Interpolation 。当需要考虑速度变化时,把插值放在极坐标系下效果更好。虽然这需要 O(n2) 的算法复杂度,但通过一张预运算的表,就可以减少到常数时间。

btw, 即使是在 state 中,也可以用到动画的混合。比如,负伤走路的角色和正常走路的角色表现不一样。负伤走路的动画可以是由负伤动画和行走动画叠加混合而来。

CppCon 2023 Writing a Better std::move -- Jonathan Müller

作者 Blog Staff
2024年7月2日 02:23

cpp23-muller.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

Lightning Talk: Writing a Better std::move

by Jonathan Müller

Summary of the talk:

std::move allows the creation of const rvalue references, which is almost always wrong. It also allows moving out of lvalue references, which can be dangerous since you don't have real ownership over them and a caller might not expect the object to disappear. Let's fix those problems using macros, reflection, and more macros.

C++ programmer's guide to undefined behavior: part 2 of 11

作者 Andrey Karpov
2024年6月30日 19:31

Your attention is invited to the second part of an e-book on undefined behavior. This is not a textbook, as it's intended for those who are already familiar with C++ programming. It's a kind of C++ programmer's guide to undefined behavior and to its most secret and exotic corners. The book was written by Dmitry Sviridkin and edited by Andrey Karpov.

C++ programmer's guide to undefined behavior: part 2 of 11

by Dmitry Sviridkin

From the article:

The compiler can be guided by the following logic: If the h value is positive—regardless of the c character—the h*27752 + c value will be positive: the c value is small, and there is no overflow. At the first iteration, h is positive, we sum up positive numbers. There are no overflows in a correct program, so at each iteration, the value will be positive. The result will be positive; we no need any check.

More on Harmful Overuse of std::move -- Raymond Chen

作者 Blog Staff
2024年6月30日 05:46

RaymondChen_5in-150x150.jpgIn recent discussions around the use of std::move in C++, questions have arisen regarding its potential overuse and the compiler's treatment of its return values. Addressing concerns raised by developers like Jonathan Duncan, this article delves into the nuances of std::move, examining whether its current implementation aligns with compiler optimizations and proposing potential enhancements for more efficient code generation.

More on harmful overuse of std::move

by Raymond Chen

From the article:

Some time ago, I wrote about harmful overuse of std::move. Jonathan Duncan asked,

Is there some side-effect or other reason I can’t see return std::move(name); case isn’t possible to elide? Or is this just a case of the standards missing an opportunity and compilers being bound to obey the standards?

In the statement return std::move(name);, what the compiler sees is return f(...); where f(...) is some mysterious function call that returns an rvalue. For all it knows, you could have written return object.optional_name().value();, which is also a mysterious function call that returns an rvalue. There is nothing in the expression std::move(name) that says, “Trust me, this rvalue that I return is an rvalue of a local variable from this very function!”

Now, you might say, “Sure, the compiler doesn’t know that, but what if we made it know that?” Make the function std::move a magic function, one of the special cases where the core language is in cahoots with the standard library.

This sort of in-cahoots-ness is not unheard of. For example, the compiler has special understanding of std::launder, so that it won’t value-propagate memory values across it, and the compiler has special understanding of memory barriers, so that it won’t optimize loads and stores across them.

CppCon 2023 Linkers, Loaders and Shared Libraries in Windows, Linux, and C++ -- Ofek Shilon

作者 Blog Staff
2024年6月29日 02:20

cpp23-shilon.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

Linkers, Loaders and Shared Libraries in Windows, Linux, and C++

by Ofek Shilon

Summary of the talk:

This talk would give a crash-intro to linkers, loaders and the layout of program binaries, and explore just enough internals to understand some observable differences in C++ builds between Linux and Windows.

We will discuss the GOT, the PLT, symbol visibility, interposition, lazy binding and more. There will be a lot of details, but also a lot of 'why's and opinions.

We will also touch/rant on what the C++ standard has to say on adjacent matters. There's a good chance you've heard before "shared libraries are outside the scope of the standard", but it doesn't mean what you think it does.

小程序可测性能力建设与实践

测试活动从本质上可以视为被测系统因为某个激励产生相应的响应,并对这些响应进行全面检测的过程。这个过程(激励->响应->检查)涉及到两个角色:测试者以及测试对象,测试者执行激励与检查响应,由机器(程序)或者人来完成;被测对象接受激励,产生响应。从这个过程来看:激励可控,响应可观,称之为可测。以实际业务测试为例,修改缓存、网络请求MCOK、页面跳转、用户登录态设置等都属于可测性能力。

在未经过任何可测性改进的终端产品中,测试人员只能通过UI交互,从UI界面观察来完成最基本的质量保障。然而应用内部存在各种各样复杂的逻辑、状态,要进行更加深入的测试则需要对这些信息进行介入与观测。例如,在进行打点测试时,操作页面后,需确认打点信息是否被正常上报,这一过程通常依赖网络代理调试工具来完成校验。同样,在用户登录测试环节中,登录完成后,需要检查缓存是否已正确记录登录信息,这要求具备缓存查看的能力,这些体现了实际业务测试场景对可测性能力的需求。

整体而言,完备地构造出目标场景进行测试涉及到多个复杂的方面,同时观测它是否符合预期也比较困难,如下图所示。终端测试长期面临着挑战。为应对这些挑战,我们以增强可测性为基础,将其贯穿测试活动的始终,使得测试能更细粒度地检查系统,提高测试深度和效率。

作为终端产品的一种形态,小程序是运行在宿主应用(如微信、快手、百度等)之上的“轻应用”,在2017年由微信推出后发展迅速。由于小程序非常依赖于宿主应用环境,因此在测试过程中,除了面临终端测试固有的难点外,它还存在一些特殊的影响因素。

从运行机制的角度来看,小程序的代码逻辑运行在宿主应用提供的容器环境内,它无法直接控制宿主应用本身和手机系统,这在一定程度上增大了测试与可测性改进的难度。

在目前的实践中,针对小程序的测试主要存在以下几种工具和策略:

  1. 采用如Charles、Fiddler等网络代理工具进行HTTP/HTTPS请求和响应的代理分析与校验。虽然这类工具适合进行数据包的抓取和分析,但它们通常无法深入小程序的内部架构,因此无法全方位控制或感知应用的内部状态。
  2. 运用图像处理技术的自动化测试工具如Airtest进行测试,它们主要关注于界面层面的操作,未能触及应用程序背后的逻辑处理,因此仍属于“黑盒测试”的范畴。
  3. 利用微信官方提供的Minium小程序测试工具来执行更为精细的测试操作,能够进行诸如API Mocking等内部控制。然而,该方法操作复杂,并依赖于微信开发者工具,而后者与真机环境之间存在一定差异,可能影响测试结果的准确性。
  4. 开发专用的自研调试面板用以验证程序逻辑和测试特定场景,但这些工具设计时常常专注于特定小程序,不易迁移至其他应用,而且它们通常不支持自动化测试流程。

综上所述,尽管存在多种测试工具和方法,但目前尚缺乏一套综合性的、易于使用的测试工具集,能够全面提升小程序的可测性。

2. 小程序可测性介绍

终端可测性能力全景图

小程序可测性的目标在于构建一套全方位的通用小程序可测性能力集合。该体系无缝支持真机和模拟器环境,兼容多端、多平台,并允许不同应用以低成本轻松接入。它能深入核心,为小程序提供全面而多元的可观测性与可控性,覆盖应用界面、内部状态、存储等关键领域。这一体系旨在赋能测试者更便捷地应对复杂测试场景,显著提高测试的效率与深入度。

经过了长期的建设积累,目前我们已经构建了一套比较全面的终端可测性能力集,包含Android、iOS、小程序、Web等技术栈。其中小程序由于系统的结构特殊性,可测性能力相对其它端会有一些不同。小程序可测性主要包括业务逻辑可测性、应用可测性、系统&设备可测性三个层级,在每个层级中包含多个垂直的细分方向,除了支持多技术栈的公共可测性能力,还提供了如AppData、宿主应用信息可观可控等特有能力。下面以几个典型能力说明小程序可测性使用方式与效果。

2.1 使用方式与效果

在实际的手工以及自动化测试工作中,小程序可测性能力能够很方便的使用,并在多个场景下发挥了重要的作用。

2.1.1 手工测试

下面将以缓存管理、页面跳转功能为例介绍小程序在手工测试中的使用方式以及效果。

在实际的测试工作中,会结合Lyrebird使用小程序可测性,Lyrebird是美团到店研发平台自研的终端测试工作台,包含终端状态数据管理、网络请求代理与Mock、缺陷记录、自定义插件扩展等能力。同时它还提供了图形化操作界面,是手工与自动化测试中使用可测性能力的入口。

在小程序接入可测性能力SDK之后,可以通过可测性SDK提供的扫码功能与Lyrebird建立连接,后续就可以通过Lyrebird在PC端利用可测性对小程序进行控制以及观测。

缓存管理

我们可以通过缓存管理功能验证依赖缓存的业务逻辑正确性,如表单信息\用户信息暂存到缓存功能等。

  • 如下图所示,1处为缓存编辑框,展示当前选择设备上的小程序所有的缓存信息,并对这些缓存进行管理,支持批量的增删改。
  • 2处展示目标小程序的缓存变更事件信息,包括在该页面对缓存的编辑以及小程序自身内部对缓存的增删改操作事件,会随着事件的触发实时更新。

页面跳转

页面跳转是小程序业务测试中重度使用的能力,可以利用该功能跳转到如表单页,商品详情页等中间页面,不再需要从首页一步一步操作进入目标被测页面,减少测试前置准备工作,具体可以在该Lyrebird页面中输入页面路径进行跳转。

2.1.2 自动化测试

将可测性能力结合Lyrebird应用于自动化测试。如通过页面跳转能力直达测试场景,然后利用通过可测性录制的页面状态数据进行场景状态还原后进行页面渲染,获取页面上的数据/布局展示,最后将实际运行图和预先设置好的页面基准图进行对比,提供渲染的差异结果,进行视觉DIFF测试。

这类“视觉测试”以页面为单位,通过深度链接跳转技术配合一系列终端应用本身的可测性改进,直达测试场景,并通过图像处理技术如长图融合、图像增量对比和文本识别能力进行视觉DIFF测试。

可测性建设的是对应用内部状态的可观可控能力,对于任何测试方法,只要涉及应用内部,可测性都能发挥重要作用。比如在健壮性测试中通过可测性构造破坏性异常场景,或者在功能测试中模拟小程序不同的进入方式(如二维码、视频号、搜索等)来测试所有可能的使用场景下小程序的运行情况。

2.2 接入方式

小程序可测性能力SDK被封装为一个NPM包,在小程序源代码或者编译产物项目中引入此NPM包,便可实现可测性能力的接入,无需进行额外适配工作。

跨平台运行

除了对微信小程序的支持之外,小程序可测性能力SDK通过集成一个适配器(Adapter)将能力扩展到多个宿主应用,包括美团、支付宝、快手、百度等平台的支持。这些平台的基础库API与微信类似,适配器会根据不同平台的特点,对代码进行相应的调整,包括基础库API、前端语法或文件类型等,以保证在各个平台上的兼容性和一致性,实现跨平台运行。

2.3 实现原理

小程序可测性实现的核心思路是通过JavaScript Hook的方式,在小程序JavaScript Runtime中对如微信小程序JS基础库、业务公共基础组件等目标模块进行透明化介入,实现对其内部的可观可控。在此之后,通过可测性SDK内的中控与外部建立网络链接,从而实现在远端对小程序内部状态与功能的可观可控。

JavaScript Hook介绍

JavaScript Hook基于JavaScript的动态特性,有以下方法:

函数Hook:直接覆盖或修改原函数:

let _originAlert = alert;  // 保存原函数
alert = function () {
  console.log('alert执行开始');
  _originAlert.apply(this, arguments); //执行原函数
  console.log('alert执行结束');
}

对象属性Hook:通过Object defineProperty定义新的或直接修改某个对象的属性,如修改Getter/Setter方法,控制对某个对象的获取/设置流程。

Object.defineProperty(document, 'cookie', {
  set: function(val) {  // 控制cookie的设置流程
    console.log('获得cookie: ', val);
    currentCookie = val;
    return val;
  },
  get: function() {  // 控制cookie的获取流程
    return null;
  }
});

原型链Hook:修改原型链上的数据,如StringDate

let _originalGetTime = Date.prototype.getTime;  // 保存原型链原方法
Date.prototype.getTime = function() {
  console.log('getTime has been called'); 
  return originalGetTime.apply(this, arguments); //执行原方法
};

Proxy对象:创建代理模式替代原始对象,可以重新定义获取、设置和定义属性等基本对象操作。

// 创建Proxy有两个参数:
// target:要代理的原始对象
// handler:定义哪些操作将被拦截以及如何重新定义被拦截操作的对象
let handler = {
  get: function(target, prop) {
    console.log(`获取 ${prop}`);
    return target[prop];
  },
  set: function(target, prop, val) {
    console.log(`设置 ${prop} 值为 ${val}`);
    target[prop] = val;
    return true;
  }
};

let proxy = new Proxy(window, handler);

proxy.test = 'test';     // 输出: Setting test to test
console.log(proxy.test); // 输出: Getting test
                         //      test   
                         

静态Hook:小程序构建时在特定文件中直接修改其JavaScript源代码。

其他方式这里就不详细展开了。

可测性SDK的大体可分为四层:

  • 通信层:与外部进行通信,负责指令和数据与远端(如Lyrebird)的双向流动。
  • 指令分发层:对通信层接收到的参数指令进行解析,依次调用控制小程序相关状态的功能层模块。
  • 功能层:实现小程序特定功能可观可控的业务逻辑,包括UI、网络请求、存储、应用状态等模块,实现如请求代理与修改、切换登录态或者控制缓存可测性功能。
  • Hook层:实现对实际逻辑模块状态和方法的透明化介入。由于小程序应用内部的状态/数据与开发者代码相关联,Hook层通过JavaScript Hook对宿主应用基础库、公共组件、业务特定逻辑三种类型的功能模块进行拦截介入,使得其状态/数据可观和可控,为功能层提供实现基础。Hook层一般需要先于业务代码加载,保证拦截的有效性。
    1. 宿主应用基础库。通用性改造,对小程序容器提供的系统级接口进行介入,如网络请求、地理信息等。
    2. 公共组件。组件级通用,如美团的公共登录组件,对其进行改造后,接入登录组件的小程序都能够使用相应的可测性能力,比如切换登录态/模拟登出等能力。
    3. 业务特定逻辑。某个小程序特有的业务逻辑,通过可测性SDK提供的API对这些逻辑进行改造后以插件形式集成定制化能力。

下面将以网络请求可观可控为例介绍小程序可测性的实现原理。

网络请求代理

当外部希望控制小程序设置网络代理时,整体流程如下:

  1. 外部(人/机器)首先通过HTTP/WebSocket方式传递包含设置小程序请求代理的指令,如图即拦截小程序发送的请求转发到127.0.0.1:1234代理服务器;
  2. 可测性SDK在通信层接收相应的指令后。将其传递给指令分发层。在指令分发层中,收到指令后进行解析,并按预定规则对指令执行进行编排,确定执行顺序;
  3. 指令分发层按编排顺序调用功能层设置网络代理并传入开启状态和代理服务器地址参数,功能层通过修改这两个变量,控制Hook层对请求API的拦截,从而改变请求代理的状态;
  4. Hook层拦截微信基础库里wx对象的request方法,如下图代码所示,分为以下流程:

    a.保存wx.request原始方法的引用(3行),并通过Object.defineProperty将wx对象设置为可写状态(4-8行);

    b.将wx.request修改为Hook的新方法。新方法的入参与原始wx.request一致,包括请求头、请求地址、响应体等,因此可以对这些参数进行修改(12行),比如替换请求域名、增加请求头、修改响应体数据等;

    c.最后用修改后的参数使用原始方法进行执行(13行)。

Hook层通过mockStatus和mockUrl两个变量控制到小程序是否被代理以及代理服务器地址(19-22行),当开发者代码中使用wx.request发起请求时,会先经过Hook指向的新方法。如果被设置代理,请求地址将会根据代理服务器协议进行修改,从而使得请求被代理。

3. 美团门票业务小程序测试实践

在到店众多应用了小程序可测性能力的业务中,美团门票业务从2021年开始即参与了小程序可测性建设,目前在门票质量保障工作中,可测性相关能力均深度应用在新需求测试、回归测试、线上巡检等各种类型的测试活动中。

3.1 可测性落地

下面通过门票业务一个具体的新需求测试例子来介绍可测性如何在测试活动中进行落地。

需求背景

用户从商品详情页进入到填单页,在选择日期、数量或填写游玩人等信息后,为了减少用户的操作路径,再次进入该填单页需要保持之前填写的这些信息不变。

操作路径划分

该过程需要经过以下步骤:进入填单页 —> 打开价格日历弹层,选择相应的日期 —> 添加数量 —> 填写或者选择游玩人 —> 点击返回退出填单页 —> 再次进入填单页,查看它当前的状态。我们选择对缓存进行可测性改进,依靠指令数据驱动+内部方法调用来达到同等UI操作的效果,保障此类场景测试的稳定性并提高执行效率。

技术实现

整体通过缓存实现。在进入填单页时,首先会读取小程序上的缓存并渲染;在选择日期、数量和游玩人时,分别对相关信息进行暂存;在退出填单页时,将这些暂存的数据写入缓存。

测试分析

由于进入填单页需要读取缓存进行渲染,因此测试过程中首先应从UI上进行验证,判断第二次进入的日期、数量和游玩人是否与上一次进入时选择的状态一致;其次还应从数据上进行验证,即进入填单页有“读”缓存的动作;在退出填单页时,需要将暂存的数据写入缓存,因此测试过程中应验证数据能正确地写入缓存,而且缓存里有正确的值。

可测性能力实践落地

  • 通过可观校验“写”的正确性。对于“写”,验证缓存的写入动作,并且写入缓存的数据是正确的。缓存的可观性改造能够将“写”的动作、“写”的当前值以及当前缓存具体信息,进行上报,这样就可以自动化校验当前操作后是否缓存值是否发生了正确的变化,以此完成对缓存“写”的校验。

  • 通过可控校验“读”的正确性。对于“读”,首先验证UI能够正确展示,其次从数据上验证有缓存的“读”动作。由于测试缓存必须经历选择日期、选择数量、选择游玩人,返回退出填单页等多个步骤。测试路径较为繁琐,因此,对缓存的可控性改造后,传入相应的配置指令(如2.2部分介绍),控制缓存中的数据,直达被测页面和状态,并通过自动化测试比对当前运行的页面和页面基准图,判断它是否正确被渲染,以此分别从数据和UI上完成对缓存“读”的校验。

门票业务在小程序测试上目前已经落地多种可测性能力,如下图所示,包括控制页面跳转、请求代理、控制登录、日志上报、隐私治理、前后端环境、录制回放、自动化交互控制等都在门票测试活动中有相应的落地,发挥着非常重要的作用。

3.2 业务实践总结

门票业务借助可测性改进使得测试的覆盖更加全面,目前30%+的测试场景依赖于可测性能力进行构建。在美团小程序和点评小程序的门票频道以及门票独立小程序上均有上百个自动化测试用例,页面覆盖率已经达到100%,场景覆盖程度达到80%+。这些测试用例在门票新需求测试、回归测试等各个阶段都会触发自动执行,累计已辅助发现上百个有效问题。

4. 总结与展望

美团核心本地商业/到店研发平台从2021年开始系统化建设小程序可测性,到目前融入到店终端测试工具链以及质量保证体系之中,通过具备扩展性的通用能力框架,融合手工和自动化测试,贯穿测试活动始终。未来我们还将持续关注于基础可测性能力的稳定性,聚焦具备更多业务特性的可测性能力建设。

Q&A

Q:代理逻辑如果有Bug会不会影响比较大

A:代理逻辑本身很简单,出错概率不大。进行Hook时,会有异常监控能力以及相应的兜底策略,即使出问题,也尽量降低对业务实际使用的影响。

Q:可测性SDK需要对业务代码进行改造吗?

A:不需要,可测性SDK对于业务应用是透明的。

Q:Lyrebird项目和小程序可测性SDK的关系是什么?

A:Lyrebird与小程序可测性是两个独立的项目。小程序可测性SDK是以一个NPM包的形式实现的,在小程序里安装NPM包,即可使小程序具有可测性。Lyrebird可以与小程序可测性SDK的通信接口进行连接,然后用户可通过Lyrebird中小程序可测性页面使用小程序可测性能力。

Q:针对小程序可测试性能力建设与实践,我想问下,如果我们要用你们的测试工具,需要做什么适配吗?

A:不需要进行额外适配,最终的呈现会是NPM包形式,在产物里安装就可以接入我们的可测性能力,可以对它进行控制。

Q:生产环境会接入可观测SDK吗?如果接入对性能有多大影响?

A:首先是对它的性能的影响,我们实际上是对小程序里的基础库的API或者一些状态数据进行了拦截,会对性能产生一定的影响,但目前这个影响范围对业务来说比较小,是可接受的。生产环境的不会引入可测性SDK,因此不会对线上质量造成影响。

Q:小程序可测性有不适合使用的场景?

小程序可测性主要针对小程序前端手工与自动化场景进行能力提升,它是具备一套通用可扩展框架,可以按照业务需求低成本进行可测性能力扩展,然而,存在特定情况下其适用性受限:首先,由于运行环境的约束,针对宿主应用如微信或支付宝自身的可测性需求,小程序的可测性无法支持。此外,小程序可测性专注于终端测试,因此对于那些需求后端服务链路验证的场景,并不适用,需配合针对性工具使用。

CppCon 2023 Libraries: A First Step Toward Standard C++ Dependency Mgmt--Bret Brown & Bill Hoffman

作者 Blog Staff
2024年6月28日 02:15

cpp23-brown.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

Plenary: Libraries - A First Step Toward Standard C++ Dependency Management

by Bret Brown & Bill Hoffman

Summary of the talk:

Prebuilt libraries have existed for decades… they even predate C++! After all these years, techniques to use prebuilt libraries are still ad hoc and difficult to maintain. A root cause of this variety of techniques is the variety of things that are C++ libraries: header-only libraries, statically-linked archives, dynamically-linked binaries, and so on. The consuming projects need to build against these libraries in consistent ways or risk unproductive workflows – and potentially, even catastrophic failure in production environments. This lack of convergence creates enormous interoperability problems across broad portions of the worldwide programming ecosystem, not just the C++ parts of it.

This talk will explore the complexities of defining what is a “C++ library.” It will then present the joint work of Kitware, Bloomberg, and others toward a preliminary design for creating initial standards for dependency management in C++ – metadata files to describe prebuilt libraries. A roadmap for maturing the design will also be shared, including proposing a standard definition for C++ libraries, building on previous proposals such as P1313: Package Specification (https://wg21.link/P1313).

This talk is intended for anyone who produces, maintains, or consumes C++ libraries. Special knowledge of C++ tooling, build systems, or package managers is not required.

Sean Baxter: Safe C++

2024年6月27日 22:34

Sean Baxter demonstrates memory safe C++ using his Circle compiler

Safe C++
Sean Baxter

From the talk:

Does a subset of a superset of C++ exists that achieves similar safety guarantees to rust, is useful and expressive enough, and is compatible with today's C++? If so, is anyone mad enough to do it? There is an answer to that

Qt and Trivial Relocation (Part 2) -- Giuseppe D'Angelo

作者 Blog Staff
2024年6月26日 06:09

kdab.pngIn this installment we are going to explore the relationships between trivial relocation and move assignments.

Qt and Trivial Relocation (Part 2)

by Giuseppe D'Angelo

From the article:

Last time we started our investigation of trivial relocation by considering an important use-case: reallocating a vector. This happens when a vector reaches its capacity, but more storage is needed.

Let’s now consider a different operation: erasing an element from the middle of a QVector.

How do we go about it?

isocpp-dangelo.png

《刻意练习》

作者 Escape
2024年6月24日 20:10

一本记录孩子日常生活的碎碎念,意义不大!

《刻意练习:如何从新手到大师》是由安德斯·艾利克森(Anders Ericsson)和罗伯特·普尔(Robert Pool)合著的一本书。书中主要探讨了通过刻意练习,普通人如何能够实现卓越表现。

《刻意练习》读书笔记


1. 书籍评分

🙋‍♀️ 总结感悟:记录你的阅读感悟!

阅读状态书名作者豆瓣评分推荐指数
📓《刻意练习✍🏼 安德斯·艾利克森/罗伯特·普尔🎟 豆瓣评分:7.8👍🏼👍🏼👍🏼

2. 思考总结

记录摘抄:书中很多句子和自己内心的感悟,可以让你明白很多知识边界!

本书的前半部分描述了刻意练习是什么,为什么管用,以及杰出人物如何运用它来发展杰出的能力。本书的最后部分,让我们从对杰出人物的研究中受益,特别是对刻意练习有了深入的了解,并解释了这对我们其他人来说到底意味着什么。而刻意练习是一种有针对性的、系统性的练习方法,旨在提升个人在某一特定领域的技能。它不同于一般的重复练习,更强调质量和方法。

  • 有目的的练习的定义:为任何行业或领域策划和设计训练方法的最佳方式

    • 有目的的练习具有四个特征,使得它与所说的“天真的练习”区分开来。所谓“天真的练习”,基本上只是反复地做某件事情,并指望只靠那种反复,就能提高表现和水平。
    • 有目的的练习:走出你的舒适区,但要以专注的方式制订明确的目标,为达到那些目标制订一个计划,并且想出监测你的进步的方法。还要想办法保持你的动机。
    • 有目的的练习还不够:刻苦努力还不够。逼迫自己超越极限,也不够。人们通常忽略了训练与练习中的其他一些同等重要的方面。学术界对一种特定的练习与训练方法进行了研究,该方法已被证明是提高人们在各个行业或领域中的能力的最强大和最有效的方式,这种方法就是 刻意练习
  • 有目的练习的四个特点

    • 有定义明确的特定目标
      • 定义明确的具体目标,可以有效地用于引导你的练习。要把目标分解,并制订一个计划。一个合理的具体目标,但你甚至还得将它进一步分解:学习乐理知识的步骤。怎么做到?可以请一位教练来教你怎样以特定方式改一改你的挥拍动作。诸如此类。关键是接受那个一般目标(并且日渐精进),并将其转化成一些具体目标,使你能达到切合实际的进步的期望。
    • 练习时需要集中注意力
      • 练习时要全神贯注,避免分心。要想取得进步,必须完全把注意力集中在你的任务上,并非每个人都通过高声叫喊和拍桌子的方式集中注意力。
    • 有目的的练习包含反馈
      • 一般而言,不论你在努力做什么事情,都需要反馈来准确辨别你在哪些方面还有不足,以及怎么会存在这些不足。如果没有反馈,你不可能搞清楚你在哪些方面还需提高,或者你现在离实现你的目标有多远。需要及时得到有效的反馈,以便不断调整和改进自己的表现。
    • 有目的的练习走出舒适区
      • 如果你从来不迫使自己走出舒适区,便永远无法进步。试着做不同的事情,而非更难的事情。并非达到极限,而是动机不足。
      • 一般来讲,有意义的正面反馈是保持动机的关键要素之一。这种反馈可能是内部反馈,也可能是由其他人提出的外部反馈,对提高自己的水平十分重要。
      • 一旦某个人的表现达到了“可接受”的水平,并且可以做到自动化,那么,再多“练习”几年,也不会有什么进步。原因在于,如果没有刻意地去提高,这些自动化的能力会缓慢地退化。
  • 如何运用刻意练习原则

    • 最大限度地运用刻意练习原则
      • 如果在你所处的行业或领域之中,刻意练习可以实行,那么,你应当采用刻意练习。如果不是,那就要尽最大的可能应用刻意练习的原则。
    • 最佳方法是找到优秀导师
      • 不论什么时候,只要有可能,最佳方法几乎总是 找一位优秀的教练或导师。刻意练习与其他类型的有目的的练习在两个重要的方面上存在差别。首先,它需要一个已经得到合理发展的行业或领域,其次,需要一位能够布置练习作业的导师。
  • 大脑的适应能力:建立检索结构

    • 大脑的适应能力的定义
      • 大脑的结构与运行都会为了应对各种不同的心理训练而改变,很大程度上像你的肌肉和心血管系统响应体育锻炼那样。对于刻意练习,我们的目标不仅仅是发掘自己的潜能,而且要构筑它,以便从前不可能做到的事情变得可能做到。
    • 大脑适应能力的应用
      • 训练可以改变大脑结构:越来越多证据表明,大脑的结构与运行都会为了各种不同的心理训练而改变。而有目的练习刻意训练,既能增强身体能力,又能强化心理能力,使我们可以做一些从未做过的事情。
      • 挑战越大变化越大,但不要太过:大脑对于处在舒适区之外却离得并不太远的“甜蜜点”上的挑战,改变最为迅速。
      • 训练如何改变大脑需要持续:由训练引起的认知和生理变化需要继续保持。如果停止训练,它们便开始消失。
    • 大脑训练达到的目的
      • 联系需要 建立检索结构,这样可以避免短时记忆的局限,并马上就能高效地处理大量信息。建立一系列的心理结构,通过运用长时记忆,避开了短时记忆的局限,从而记住更多知识。
  • 心理表征:有助于刻意练习

    • 心里表征的定义
      • 心理表征是一种与我们大脑正在思考的某个物体、某个观点、某些信息或者其他任何事物相对应的心理结构,或具体或抽象。
    • 心里表征的重要性
      • 心理表征有助于处理信息:理解和解读它,把它保存在记忆之中,组织它、分析它,并用它来决策。
      • 心理表征有助于组织信息:对于所有的心理表征,有一点是相同的:尽管短时记忆存在局限,但它们使得人们可以迅速地处理大量信息。
      • 心理表征有助于制订计划:心理表征可以用来为很多行业和领域做计划,表征越好,计划就高效。
      • 心理表征有助于高效学习:学生之间的差别,在很大程度上最有可能取决于他们能多敏锐地察觉自己所犯的错误,也就是说,他们对音乐作品的心理表征有多么有效。
    • 心理表征的发展过程
      • 通过对自己的心理表征进行持续的检测和评估,人们可以在必要时调整和优化自己的心理表征,以提高其效率和效果。
      • 技能与心理表征之间的关系是一个良性循环:你的技能越娴熟,创建的心理表征就越好;而心理表征越好,就越能有效地练习,以磨炼技能。
  • 在工作中运用刻意练习原则

    • 让练习变成日常工作的一部分
      • 第一种错误思想:即认为某人的能力通常受到基因特征的限制。这种思想常常表现为各种各样“我不能”或者“我不是”之类的表述。在任何一个人们选择着重发展的行业或领域之中,人人都可以通过正确的训练来帮助自己大幅度地提高。我们可以塑造自己的潜力。
      • 第二种错误思想:如果你足够长时间地做某件事情,一定会更擅长。这种思想错在哪里,我们很清楚。以完全相同的方式一而再再而三地做某件事情,并不是提高绩效和表现的秘诀;它会使人们停下前进的脚步,并且缓慢地下滑。
      • 第三种错误思想:要想提高,只需要努力。如果足够刻苦,你会更加优秀。但现实是,所有这些事情,即管理、销售和团队合作,全都是专业化的技能,除非你运用一些专门用于提升那些特定技能的练习方法,否则,即使加倍努力,也无法让你有更大的进步。
    • 边干边学
      • 好处是,它使人们熟悉练习的习惯,并思考如何练习。一旦他们理解了日常练习的重要性,并意识到可以用练习来实现多大的进步,那么,他们会找机会将其他的日常商业活动转变成练习活动。到最后,练习变成了日常工作的一部分。
    • 知识与技能之间的区别
      • 知识与技能之间的区别,正是发展专业技能的传统路径与刻意练习的方法之间的核心差别。传统的方法一直是先找出关于正确方法的信息,然后让学生运用那些知识。刻意练习则只聚焦于绩效和表现,以及怎样提高绩效和表现。在专业的或商业的背景中涉及提高绩效和表现时,正确的问题是“我们怎样改进相关的技能”,而不是“我们怎样传授相关的知识”。
  • 在生活中运用刻意练习原则

    • 首先,找位好导师
      • 尽管好导师并不一定是世界上最出色的人,但他应当在行业或领域之中有所成就。一般来讲,导师只是能够引导你达到他们或者他们的学生曾经达到过的水平。如果你是一位不折不扣的初学者,那么,只要导师具有足够熟练的技能,对你来说都可以,但如果你已经训练了好几年,那你需要找一位更能干的导师。
    • 如何寻找好导师
      • 当你自己改变了时,可能需要更换导师。
    • 专注和投入至关重要
      • 要记住:如果你在走神,或者你很放松,并且只为了好玩,你可能不会进步。而不专注,练习也是没效果的。制订明确的目标,把练习课程的时间缩得更短,是更加迅速地提升新的技能水平的最佳方式。
    • 没有导师,怎么办
      • 设计有效的联系方法:反复做一件事情,目的是找出你在哪些方面存在不足,并且聚焦于在那些方面取得进步,试着采用不同的方法来提高,直到你最终找到适合自己的方法。
      • 将技能分解成一些组成部分,以便反复地练习,并且有效地分析、确定你的不足之处,然后想出各种办法来解决它们。我们只有努力去复制杰出人物的成就,失败了就停下来思考为什么会失败,才能创建有效的心理表征。
      • 跨越停滞阶段的方法,可以以新的方式挑战自己。同时,想办法稍微逼自己一下,但不要逼得太狠,这通常会帮助你搞清楚自己的“停滞点”在什么地方,克服攻克特定的弱点。
    • 保持动机
      • 意志力根本不存在:意志力并非决定我们是否能够继续进行刻意练习的关键,动机才是。动机包含两个组成部分:继续前进的理由和停下脚步的理由。你要保持动机,要么强化继续前行的理由,要么弱化停下脚步的理由。
      • 弱化停下脚步的理由:为了弱化停下脚步的理由,可以设定固定的联系时间,避免其他事情的干扰,以及保证足够的睡眠和健康。
      • 增强继续前进的倾向:一旦你已经练习了一段时间,并且可以看到结果了,这种技能本身就可以成为你动机的一部分;将对同一件事情感兴趣的所有人聚集起来,或者吸引他们加入一个现有的团体,并且将团体的同志情谊和共同的目标作为达到你自己目标的额外动机;
      • 精心设置目标:将漫长的旅程分解成一系列可控的目标,并且每次只关注它们中的一个,甚至可以在每次达到一个目标时,给自己小小的奖励。
  • 成为杰出人物的路线图

    • 杰出人物成长三阶段
      • 第一阶段 产生兴趣:孩子们发展了对某一特定领域或行业的兴趣,那便是:他们觉得棋子好玩,把它们当成玩具,一件玩耍的东西。孩子们发展了简单的技能,例如把国际象棋棋子摆到棋盘上、把篮球投进篮筐、挥舞球拍等。
      • 第二阶段 变得认真:一旦未来的杰出人物对某个行业或领域感兴趣了,而且似乎在其中有着美好的发展前景,下一步通常需要到教练或导师那里上课了。学生面临的期望也逐渐升高,直到他们基本上能够尽最大的可能改进为止。
      • 第三阶段 全力投入:在某些行业或领域,如音乐表演或芭蕾舞蹈等,刻意练习是必不可少的。在其他领域,如医学、教育和企业管理等,尽管无法进行刻意练习,但仍可以运用刻意练习的原则,指引自己发展在所处行业或领域之中可能的最佳的方法几乎总是找一位优秀的教练或导师。
      • 第四阶段 开拓创新:创造性总是保有某种神秘感,因为就其本身而言,创造就是制造出人们尚未见过或体验过的事情。那些有创造性的、不安分的、有进取心的人总是不满足于现状,他们寻找各种办法来向前推进,做一些别人没有做过的事情。
    • 成长需要注意的事情
      • 父母的重要性:那些日后成为杰出人物的孩子,其父母在孩子的成长和发展阶段中扮演了至关重要的角色。尽管父母和导师可以采用许多方法来激励孩子,但动机必须来自孩子的内心,否则,它不会长久。那些长大后成为才华横溢的艺术家的孩子,有着“自加燃料、自我激励的动机来从事繁重的工作”,尽管他们依然需要父母亲和导师“情绪的和技术的支持”。
      • 兄弟姐妹的激励作用:一个孩子看到自己的哥哥或姐姐在从事某项活动,并且获得父亲或母亲的关注和表扬时,自然也想加入进来,获得父母同样的关注和表扬。对某些孩子而言,和兄弟姐妹之间开展竞争,本身也很激励人。

  • 刻意练习的本质

长时记忆正是区分卓越者与一般人的一个重要能力,它才是刻意练习的指向与本质。刻意练习的任务难度要适中,能收到反馈,有足够的次数重复练习,学习者能够纠正自己的错误。

  • 隐性知识

认知复杂度是指你建构“客观”世界的能力。认知复杂度高的人具有高度复杂化的思维能力,更善于同时使用互补与互不相容的概念来理解客观世界。真实世界中,黑白对错并非截然分明。学习科学大量研究表明,成人的最佳学习方式并非独自练习,而是在情境中学习。有效学习是进入相关情境,找到自己的“学习共同体”,学习者最开始时围绕重要成员转,做一些外围的工作,随着技能增长,进入学习共同体圈子的核心,逐步做更重要的工作,最终成为专家。

  • 超越 1 万小时定律

真相是,从来不存在 1 万小时定律,它仅仅是畅销书作家对心理科学研究的一次不太严谨的演绎而已。1 万小时定律究竟有哪些问题呢?首先,不同专业领域的技能习得时间与练习时间并不存在一个 1 万小时的最低阈值,比如学习小提琴和医生所需时间就不同。其次,成功与练习时间并不完全成正比,天赋虽然在其中不起决定性作用,却也会是一大影响因子。再次,练习的成果并不与时间呈正相关,这一点,也取决于练习方法。在沙堆悖论的视野下,1 万小时定律的价值也就这样被消解了。正如真实的心理科学研究表明,成为专家的时间往往随着不同的专业技能领域而变化。

  • 天才的定义和形成

人们常常将非凡的技艺或才能归因为天赋,认为这些人生来就比被人优秀。研究表明,这些杰出人物的天才,并非天生,而是通过他们的能力和适当的方法培养出来的,而是通过深入的研究和刻苦训练。

  • 天才更懂得利用大脑的适应能力

不论基因遗传可能在“天才”取得的成就中发挥着什么作用,他们和我们一样,大脑和身体都具有适应能力,只是比我们更多地利用了那一能力而已。不论在什么行业或领域,提高表现与水平的最有效方法,全都遵循一系列普遍原则。我们把这种通用的方法命名为“刻意练习”。

  • 学习新技能的一般方法

一旦你已经达到了这种令你满意的技能水平,而且能做到自然而然地表现出你的水平,无论是开车、打网球还是烘焙饼干,你就已经不再进步了。人们通常错误地理解这种现象,因为他们自以为,继续开车、打网球或烘焙饼干,就是一种形式的练习,如果不停地做下去,自己一定能够更擅长,也许进步较为缓慢,但最终还是会更出色。人们认为,开了 20 多年车的老司机,一定会比只开了 5 年车的司机更擅长开车;行医 20 年的医生,一定会比只行医 5 年的医生更优秀;教了 20 年书的老师,一定会比只教了 5 年书的老师能力更强。

  • 有目的的练习是确定的

我们假想的音乐学生如果确定了类似下面这样的练习目标,可能会比他漫无目的的练习要成功得多:“连续三次,不犯任何错误,以适当的速度弹奏完曲子。”如果不制订这样一个目标,就没有办法判断练习是不是成功了。定义明确的具体目标,可以有效地用于引导你的练习。

  • 有目的的练习是专注的

如今,并非每个人都通过高声叫喊和拍桌子的方式集中注意力,但史蒂夫的表现说明了我们可以从有效练习的研究中获得一条重要洞见:要想取得进步,必须完全把注意力集中在你的任务上。

  • 有目的的练习包含反馈

一般而言,不论你在努力做什么事情,都需要反馈来准确辨别你在哪些方面还有不足,以及怎么会存在这些不足。如果没有反馈(要么是你自己给自己提出的,要么是局外人给你提出的),你不可能搞清楚你在哪些方面还需提高,或者你现在离实现你的目标有多远。

  • 有目的的练习需要走出舒适区

对于任何类型的练习,这是一条基本的真理:如果你从来不迫使自己走出舒适区,便永远无法进步。走出舒适区,意味着要试着做一些你以前没做过的事情。有时候,你也许发现,做一些没做过的事情,相对较为容易,然后你会继续逼迫自己。但有时候,你偶然碰到了那些让你感到很难做好的事情,似乎你永远也做不了。想办法去逾越这些障碍,是通向有目的的练习的隐藏钥匙。

  • 试着做不同的事情,而非更难的事情

通常情况下,这种解决方案并不是“试着做更难的事情”,而是“试着做不同的事情”。换句话讲,这是一个方法问题。记忆研究中一种常规的模式:首先取得进步,然后到了一个瓶颈,被困住了,寻找不同的方法来克服障碍,最后找到了这种方法,然后又稳定地提高,直到下一个障碍出现。

  • 并非达到极限,而是动机不足

在我多年的研究中,并没有找到任何清晰的证据来证明,在任何行业或领域,人们真的会遇到绩效和表现完全不变的极限。相反,我发现,人们通常会在努力提高自己的时候放弃并停下。尽管我们总能继续前进和不断进步,但要想做到,并不见得总是轻而易举。保持专注并继续努力,是很难做到的,而且通常没有趣味。

  • 有目的的练习还不够

尽管我们通过专注的训练和走出舒适区,一般能在某种程度上提高自己做某件事的能力,但那并不是全部。刻苦努力还不够。逼迫自己超越极限,也不够。人们通常忽略了训练与练习中的其他一些同等重要的方面。学术界对一种特定的练习与训练方法进行了研究,该方法已被证明是提高人们在各个行业或领域中的能力的最强大和最有效的方式。这种方法就是“刻意练习”,我们会马上进行详尽描述。但首先让我们更加密切地观察,在这种令人称奇的改进背后,到底有着怎样的原因。

  • 1 万小时法则的错与对

在任何一个有着悠久历史的行业或领域,要想成就一番事业,致力于变成业内的杰出人物,需要付出许多年艰苦卓绝的努力。也许并不需要恰好 1 万小时的练习,但要花很长时间练习。

  • 边学边干

我的基本建议是找寻一种与刻意练习原则相一致的方法,问自己以下这些问题:这种方法,是不是逼着人们走出舒适区,迫使人们尝试做一些对他们来说并不容易的事情?它有没有提供关于绩效和表现的即时反馈,以及关于可以做些什么事情来提高绩效和表现的反馈?那些制订了这种方法的人,有没有辨别出他们所处的特定行业或领域之中的最杰出人物?有没有确定是什么因素将杰出人物与其他人区分开来?训练是不是被设计用来提高行业或领域内的杰出人物所拥有的那些特定技能?如果对所有问题的回答全都是肯定的,尽管也许不能保证那种方法有效,至少可以肯定,它是有效方法的可能性大得多。

  • 破解“天才跳高运动员的神迹”

人们希望人生中有这样的奇迹,并非所有的一切都要遵循现实世界中那些古板的、令人厌倦的法则。有时,一些神奇的事情发生了,主人公一夜之间就获得了不可思议的强大力量。你可能不知道,你实际上是在氪星出生的,生下来就会飞。或者,你被一只有辐射的蜘蛛咬过一口,便能飞檐走壁了。或者,你曾暴露在宇宙射线之中,现在,你可以随时隐身了。


3. 个人思考

记录你的思考:记录你的思考过程,记录你的思考感悟!

很早以前就读过这本书了,但是当时确实是不求甚解,挑重点看的,两个多小时就读完了。最近,这本书又看了一遍,确实常看常新,尤其是前几章,特别好。确实是自己之前生活和工作中,又遇到过相同的问题,如果自己好好总结的话,确实也可以得到本文阐述的核心要点。推荐阅读!

CppCon 2023 Let's Fix Sparse Linear Algebra with C++. It'll Be Fun and Easy! -- Benjamin Brock

作者 Blog Staff
2024年6月25日 02:12

cpp23-brock.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

Lightning Talk: Let's Fix Sparse Linear Algebra with C++. It'll Be Fun and Easy!

by Benjamin Brock

Summary of the talk:

Sparse linear algebra is hard.  There are a large variety of different sparse linear algebra formats, and they all require obtuse index arithmetic in order to use.  But what if we could fix this?  In this talk, I'll present an idea for "fixing sparse linear algebra" using customization points, the ranges library, and high-level multi-dimensional iteration.

Spark向量化计算在美团生产环境的实践

让我们从一个简单问题开始:假设要实现“数组a+b存入c”,设三个整型数组的长度都是100,那么只需将“c[i] = a[i] + b[i]”置于一个100次的循环内,代码如下:

void addArrays(const int* a, const int* b, int* c, int num) {
  for (int i = 0; i < num; ++i) {
    c[i] = a[i] + b[i];
  }
}

我们知道:计算在CPU内完成,逻辑计算单元操作寄存器中的数据,算术运算的源操作数要先放置到CPU的寄存器中,哪怕简单的内存拷贝也需要过CPU寄存器。所以,完成“c[i] = a[i] + b[i]”需经三步:

  1. 加载(Load),从内存加载2个源操作数(a[i]和b[i])到2个寄存器。
  2. 计算(Compute),执行加法指令,作用于2个寄存器里的源操作数副本,结果产生到目标寄存器。
  3. 存储(Store),将目标寄存器的数据存入(拷贝)到目标内存位置(c[i])。

其中,加载和存储对应访存指令(Memory Instruction),计算是算术加指令,循环执行100次上述三步骤,就完成了“数组a + 数组b => 数组c”。该流程即对应传统的计算架构:单指令单数据(SISD)顺序架构,任意时间点只有一条指令作用于一条数据流。如果有更宽的寄存器(超机器字长,比如256位16字节),一次性从源内存同时加载更多的数据到寄存器,一条指令作用于寄存器x和y,在x和y的每个分量(比如32位4字节)上并行进行加,并将结果存入寄存器z的各对应分量,最后一次性将寄存器z里的内容存入目标内存,那么就能实现单指令并行处理数据的效果,这就是单指令多数据(SIMD)。

图1:向量化计算“数组a+b存入c”

单指令多数据对应一类并行架构(现代CPU一般都支持SIMD执行),单条指令同时作用于多条数据流,可成倍的提升单核计算能力。SIMD非常适合计算密集型任务,它能加速的根本原因是“从一次一个跨越到一次一组,从而实现用更少的指令数完成同样的计算任务。”

1996年,Intel推出的X86 MMX(MultiMedia eXtension)指令集扩展可视为SIMD的起点,随后演进出SSE(1999年)SSE2/3/4/5、AVX(2008)/AVX2(2013)、AVX512(2017)等扩展指令集。在linux系统中可以通过lscpu或cpuid命令查询CPU对向量化指令的支持情况。

1.2 向量化执行框架:数据局部性与运行时开销

执行引擎常规按行处理的方式,存在以下三个问题:

  1. CPU Cache命中率差。一行的多列(字段)数据的内存紧挨在一起,哪怕只对其中的一个字段做操作,其他字段所占的内存也需要加载进来,这会抢占稀缺的Cache资源。Cache命失会导致被请求的数据从内存加载进Cache,等待内存操作完成会导致CPU执行指令暂停(Memory Stall),这会增加延时,还可能浪费内存带宽。
  2. 变长字段影响计算效率。假设一行包括int、string、int三列,其中int类型是固定长度,而string是变长的(一般表示为int len + bytes content),变长列的存在会导致无法通过行号算offset做快速定位。
  3. 虚函数调用带来额外开销。对一行的多列进行处理通常会封装在一个循环里,会抽象出一个类似handle的接口(C++虚函数)用于处理某类型数据,各字段类型会override该handle接口。虚函数的调用多一步查表,且无法被内联,循环内高频调用虚函数的性能影响不可忽视。

图2:row by row VS blcok by block

因此,要让向量化计算发挥威力,只使用SIMD指令还不够,还需要对执行框架层面进行改造,变Row By Row为Block By Block:

  1. 数据按列组织替代按行组织(在Clickhouse和Doris里叫Block,Velox里叫Vector),这将提高数据局部性。参与计算的列的多行数据会内存紧凑的保存在一起,CPU可以通过预取指令将接下来要处理的数据加载进Cache,从而减少Memory Stall。不参与计算的列的数据不会与被处理的列竞争Cache,这种内存交互的隔离能提高Cache亲和性。
  2. 同一列数据在循环里被施加相同的计算,批量迭代将减少函数调用次数,通过模版能减少虚函数调用,降低运行时开销。针对固定长度类型的列很容易被并行处理(通过行号offset到数据),这样的执行框架也有利于让编译器做自动向量化代码生成,显著减少分支,减轻预测失败的惩罚。结合模板,编译器会为每个实参生成特定实例化代码,避免运行时查找虚函数表,并且由于编译器知道了具体的类型信息,可以对模板函数进行内联展开。

图3:向量化执行框架示例

1.3 如何使用向量化计算

  1. 自动向量化(Auto-Vectorization)。当循环内没有复杂的条件分支,没有数据依赖,只调用简单内联函数时,通过编译选项(如gcc -ftree-vectorize、-O3),编译器可以将顺序执行代码翻译成向量化执行代码。可以通过观察编译hint输出和反汇编确定是否生产了向量化代码。

    • 编译hint输出,编译:g++ test.cpp -g -O3 -march=native -fopt-info-vec-optimized,执行后有类似输出“test.cpp:35:21: note: loop vectorized”。
    • 反汇编,gdb test + (gdb) disassemble /m function_name,看到一些v打头的指令(例如vmovups、vpaddd、vmovups等)。
  2. 使用封装好的函数库,如Intel Intrinsic function、xsimd等。这些软件包中的内置函数实现都使用了SIMD指令进行优化,相当于high level地使用了向量化指令的汇编,详见:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html

  3. 通过asm内嵌向量化汇编,但汇编指令跟CPU架构相关,可移植性差。

  4. 编译器暗示:

    • 使用编译指示符(Compiler Directive),如Cilk(MIT开发的用于并行编程的中间层编程语言和库,它扩展了C语言)里的#pragma simd和OpenMP里的#pragma omp simd。
    • Compiler Hint。通过__restrict去修饰指针参数,告诉编译器多个指针指向不相同不重叠的内存,让编译器放心大胆的去优化。
  5. 如果循环内有复杂的逻辑或条件分支,那么将难以向量化处理。

以下是一个向量化版本数组相加的例子,使用Intel Intrinsic function:

#include <immintrin.h> // 包含Intrinsic avx版本函数的头文件

void addArraysAVX(const int* a, const int* b, int* c, int num) {
  assert(num % 8 == 0); // 循环遍历数组,步长为8,因为每个__m256i可以存储8个32位整数
  for (int i = 0; i < num; i += 8) {  
    __m256i v_a = _mm256_load_si256((__m256i*)&a[i]); // 加载数组a的下一个8个整数到向量寄存器
    __m256i v_b = _mm256_load_si256((__m256i*)&b[i]); // 加载数组b的下一个8个整数到向量寄存器
    __m256i v_c = _mm256_add_epi32(v_a, v_b); // 将两个向量相加,结果存放在向量寄存器
    _mm256_store_si256((__m256i*)&c[i], v_c); // 将结果向量存储到数组c的内存
  }
}

int main(int argc, char* argv[]) {
  const int ARRAY_SIZE = 64 * 1024;
  int a[ARRAY_SIZE] __attribute__((aligned(32))); // 按32字节对齐,满足某些向量化指令的内存对齐要求
  int b[ARRAY_SIZE] __attribute__((aligned(32)));
  int c[ARRAY_SIZE] __attribute__((aligned(32)));
  srand(time(0));
  for (int i = 0; i < ARRAY_SIZE; ++i) {
    a[i] = rand(); b[i] = rand(); c[i] = 0; // 对数组a和b赋随机数初始值
  }

  auto start = std::chrono::high_resolution_clock::now();
  addArraysAVX(a, b, c, ARRAY_SIZE);
  auto end = std::chrono::high_resolution_clock::now();
  std::cout << "addArraysAVX took " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << " microseconds." << std::endl;
  return 0;
}

addArraysAVX函数中的_mm256_load_si256、_mm256_add_epi32、_mm256_store_si256都是Intrinsic库函数,内置函数命名方式是

  • 操作浮点数:_mm(xxx)_name_PT
  • 操作整型:_mm(xxx)_name_epUY

其中(xxx)代表数据的位数,xxx为SIMD寄存器的位数,若为128位则省略,AVX提供的__m256为256位;name为函数的名字,表示功能;浮点内置函数的后缀是PT,其中P代表的是对矢量(Packed Data Vector)还是对标量(scalar)进行操作,T代表浮点数的类型(若为s则为单精度浮点型,若为d则为双精度浮点);整型内置函数的后缀是epUY,U表示整数的类型(若为无符号类型则为u,否在为i),而Y为操作的数据类型的位数。

编译:g++ test.cpp -O0 -std=c++11 -mavx2 -o test。选项-O0用于禁用优化(因为开启优化后有可能自动向量化),-mavx2用于启用AVX2指令集。

测试发现:非向量化版本addArrays耗时170微秒,而使用Intrinsic函数的向量化版本addArraysAVX耗时58微秒,耗时降为原来的1/3。

2 为什么要做Spark向量化计算

从业界发展情况来看,近几年OLAP引擎发展迅速,该场景追求极致的查询速度,向量化技术在Clickhouse、Doris等Native引擎中得到广泛使用,降本增效的趋势也逐渐扩展到数仓生产。2022年6月DataBricks发表论文《Photon- A Fast Query Engine for Lakehouse Systems》,Photon是DataBricks Runtime中C++实现的向量化执行引擎,相比DBR性能平均提升4倍,并已应用在Databricks商业版上,但没有开源。2021年Meta开源Velox,一个C++实现的向量化执行库。2022 Databricks Data & AI Summit 上,Intel 与Kyligence介绍了合作开源项目Gluten,旨在为Spark SQL提供Native Vectorized Execution。Gluten+Velox的组合,使Java栈的Spark也可以像Doris、Clickhouse等Native引擎一样发挥向量化执行的性能优势

从美团内部来看,数仓生产有数万规模计算节点,很多业务决策依赖数据及时产出,若应用向量化执行技术,在不升级硬件的情况下,既可获得可观的资源节省,也能加速作业执行,让业务更快看到数据和做出决策。根据Photon和Gluten的公开数据,应用向量化Spark执行效率至少可以提升一倍,我们在物理机上基于TPC-H测试Gluten+Velox相Spark 3.0也有1.7倍性能提升。

图4:Gluten+Velox在TPC-H上的加速比,来自Gluten

3 Spark向量化计算如何在美团实施落地

3.1 整体建设思路

  1. 更关注资源节省而不单追求执行加速。Spark在美团主力场景是离线数仓生产,与OLAP场景不同,时间相对不敏感,但资源(内存为主)基数大成本敏感。离线计算历史已久,为充分利用存量服务器,我们不能依赖硬件加速的手段如更多的内存、SSD、高性能网卡。我们评估收益的核心指标是总「memory*second」降低。
  2. 基于C++/Rust等Native语言而非Java进行开发。Java语言也在向量化执行方面做尝试,但JVM语言对底层控制力弱(如无法直接内嵌SIMD汇编),再加上GC等固有缺陷,还远远谈不上成熟,而系统向的语言(C/C++、Rust)则成为挖掘CPU向量化执行潜能的首选。
  3. 可插拔、面向多引擎而非绑定Spark。虽然面向不同工作负载的各类大数据引擎层出不穷,但其架构分层则相似,一般包括编程接口、执行计划、调度、物理执行、容错等,尤其执行算子部分有较多可复用实现。如Meta内部主要大数据引擎有Presto和Spark,建设一个跨引擎的执行库,优化同时支持Presto和Spark显然是更好的选择;OLAP引擎向量化计算本身就是标配;流计算引擎出于性能考虑,也可以攒批而非一条条处理数据(mini batch),因此向量化执行也有性能提升空间。我们认为面向不同场景设计的大数据引擎,有可能共用同一个高性能向量化执行库。
  4. 使用开源方案而非完全自研。Spark有几百个function和operator,向量化改造的工作量巨大,从性能、完成度、适配成本、是否支持多引擎、社区的活跃度等方面综合考虑,我们最终选择了Gluten+Velox的方案。
  5. 迁移过程对用户透明,保证数据一致。Spark的几百个function和 operator都要通过C++重新实现,同时还涉及Spark、Gluten、Velox版本变化,很容易实现出现偏差导致计算结果不一致的情况。我们开发了一个用于升级验证的黑盒测试工具(ETL Blackbox Test),可以将一个作业运行在不同版本的执行引擎上进行端到端验证,包括执行时间、内存及CPU资源使用情况、作业数据的对比结果(通过对比两次执行的行数,以及每一列所有数据md5的加和值来确定数据是否一致)。

3.2 Spark+Gluten+Velox计算流程

通过Spark的plugin功能,Gluten将Spark和向量化执行引擎(Native backend,如Velox)连接起来,分为Driver plugin和Executor Plugin。在Driver端,SparkContext初始化时,Gluten的一系列规则(如ColumnarOverrideRules)通过Spark Extensions注入,这些规则会对Spark的执行计划进行校验,并把Gluten支持的算子转换成向量化算子(如FileScan会转换成NativeFileScan),不能转换的算子上报Fallback的原因,并在回退的部分插入Column2Row、Row2Column算子,生成Substrait执行计划。在Executor端,接收到Driver侧的LaunchTask RPC消息传输的Substrait执行计划后,再转换成Native backend的执行计划,最终通过JNI调用Native backend执行。

Gluten希望能尽可能多的复用原有的Spark逻辑,只是把计算部分转到性能更高的向量化算子上,如作业提交、SQL解析、执行计划的生成及优化、资源申请、任务调度等行为都还由Spark控制。

图5:Spark+Gluten+Velox架构图

3.3 阶段划分

在我们开始Spark向量化项目时,开源版本的Gluten和Velox还没有在业界Spark生产环境大规模实践过,为了降低风险最小代价验证可行性,我们把落地过程分为以下五个阶段逐步进行:

  1. 软硬件适配情况确认。Velox要求CPU支持bmi、bmi2、f16c、avx、avx2、sse指令集,需要先确定服务器是否支持;在生产环境运行TPC-DS或者TPC-H测试,验证理论收益;公司内部版本适配,编译运行,跑通典型任务。当时Gluten只支持Spark3.2和Spark3.3,考虑到Spark版本升级成本更高,我们暂时将相关patch反打到Spark3.0上。这个阶段我们解决了大量编译失败问题,建议用社区推荐的OS,在容器中编译&运行;如果要在物理机上运行,需要把相关依赖部署到各个节点,或者使用静态链接的方式(开启vcpkg)。
cat /proc/cpuinfo | grep --color -wE "bmi|bmi2|f16c|avx|avx2|sse"
  1. 稳定性验证。确定测试集,完善稳定运行需要的feature,以达到比较高的测试通过率,包括支持ORC、Remote shuffle、HDFS适配、堆内堆外的内存配置等。本阶段将测试通过率从不足30%提升到90%左右。

  2. 性能收益验证。由于向量化版本和原生Spark分别使用堆外内存和堆内内存,引入翻倍内存的配置,以及一些高性能feature支持不完善,一开始生产环境测试性能结果不及预期。我们逐个分析解决问题,包括参数对齐、去掉arrow中间数据转换、shuffle batch fetch、Native HDFS客户端优化、使用jemelloc、算子优化、内存配置优化、HBO适配等。本阶段将平均资源节省从-70%提升到40%以上。

  3. 一致性验证。主要是问题修复,对所有非SLA作业进行大规模测试,筛选出稳定运行、数据完全一致、有正收益的作业。

  4. 灰度上线。将向量化执行环境发布到所有服务器,对符合条件的作业分批上线,增加监控报表,收集收益,对性能不及预期、发生数据不一致的作业及时回退原生Spark上。此过程用户无感知。

整个实施过程中,我们通过收益转化漏斗找到收益最大的优化点,指导项目迭代。下图为2023年某一时期的相邻转化情况。

图6:Spark向量化项目收益转化漏斗图

4 美团Spark向量化计算遇到的挑战

4.1 稳定性问题

  1. 聚合时Shuffle阶段OOM。在Spark中,Aggregation一般包括Partial Aggregation、Shuffle、Final Aggregation三个阶段,Partial Aggregation在Mapper端预聚合以降低Shuffle数据量,加速聚合过程、避免数据倾斜。Aggregation需要维护中间状态,如果Partial Aggregation占用的内存超过一定阈值,就会提前触发Flush同时后续输入数据跳过此阶段,直接进入ShuffleWrite流程。Gluten使用Velox默认配置的Flush内存阈值(Spark堆外内存*75%),由于Velox里Spill功能还不够完善(Partial Aggregation不支持Spill),这样大作业场景,后续ShuffleWrite流程可能没有足够的内存可以使用(可用内存<25%*Spark堆外内存),会引起作业OOM。我们采用的策略是通过在Gluten侧调低Velox Partial Aggregation的Flush阈值,来降低Partial Aggregation阶段的内存占用,避免大作业OOM。这个方案在可以让大作业运行通过,但是理论上提前触发Partial Aggergation Flush会降低Partial Aggretation的效果。更合理的方案是Partial Aggregation支持Spill功能,Gluten和Velox社区也一直在完善对向量化算子Spill功能的支持。

  2. SIMD指令crash。Velox对数据复制做了优化,如果该类型对象是128bit(比如LongDecimal类型),会通过SIMD指令用于数据复制以提升性能。如下图所示,Velox库的FlatVector<T>::copyValuesAndNulls()函数里的一行赋值会调用T::operator=(),调用的movaps指令必须作用于16B对齐的地址,不满足对齐要求会crash。我们在测试中复现了crash,通过日志确定有未按16B对齐的地址出现。无论是Velox内存池还是Gluten内存池分配内存都强制做了16B对齐,最终确认是Arrow内存池分配出的地址没对齐(Gluten用了三方库Arrow)。这个问题可以通过为LongDecimal类型重载operator=操作符修复,但这样做可能影响性能,也不彻底,因为不能排除还有其他128bit类型对象存在。最终我们与Gluten社区修改了Arrow内存分配策略,强制16B对齐。

图7:Crash代码示例

4.2 支持ORC并优化读写性能

Velox的DWIO模块原生只支持DWRF和Parquet两种数据格式,美团大部分表都是基于ORC格式进行存储的。DWRF文件格式是Meta内部所采用的ORC分支版本,其文件结构与ORC相似,比如针对ORC文件的不同区域,可通过复用DWRF的Reader来完成相关数据内容的读取。

图8:Dwrf文件格式

  • DwrfReader:用于读取文件层面的元数据信息,包括PostScript、Footer和Header。
  • DwrfRowReader:用来读取StripeFooter,以便确定每个column的Stream位置。
  • FormatData:用来读取StripeIndex,从而确定每个RowGroup的位置区间。
  • ColumnReader:用来读取StripeData,完成不同column的数据抽取。

我们完善了Velox ORC格式的支持,并对读取链路做了优化,主要工作包括:

  1. 支持RLEv2解码(Velox-5443)并在解码过程中完成Filter下推(Velox-6647)。我们将Apache RLEv2解码逻辑移植到了Velox,通过BMI2指令集来加速varint解码过程中的位运算,并在解码过程中下推过滤不必要的数据。
  2. 支持Decimal数据类型(Velox-5837)以及该数据类型的Filter下推处理(Velox-6240)。
  3. ORC文件句柄复用以降低HDFS的NN处理压力(Velox-6140)。出于线程安全层面的考虑,HdfsReadFile每次pread都会开启一个新文件句柄来做seek+read,客户端会向NameNode发送大量open请求,加重HDFS的压力。我们通过将文件的读取句柄在内部做复用处理(thread_local模式),减少向NN发送的open请求。
  4. 使用ISA-L加速ORC文件解压缩。我们对ORC文件读取耗时trace分析得出,zlib解压缩占总耗时60%,解码占30%,IO和其他仅占10%,解压效率对ORC文件读取性能很关键。为此,我们对ZlibDecompressor做了重构,引入Intel的解压缩向量化库ISA-L来加速解压缩过程。

基于这些优化,改造后的Velox版ORC Reader读取时间减少一半,性能提升一倍。

图9:Apache ORC与改造后的Velox ORC读取性能对比,上为Apache ORC

4.3 Native HDFS客户端优化

首先介绍一下HDFS C++客户端对ORC文件读取某一列数据的过程。第一步,读取文件的最后一个字节来确定PostScript长度,读取PostScript内容;第二步,通过PostScript确定Footer的存储位置,读取Footer内容;第三步,通过Footer确定每个Stripe的元数据信息,读取StripeFooter;第四步,通过StripeFooter确定每个Column的Stream位置,读取需要的Stream数据。

图10:ORC文件读取过程

在生产环境测试中,我们定位到两个数据读取相关的性能问题:

  1. 小数据量随机读放大。客户端想要读取[offset ~ readEnd]区间内的数据,发送给DN的实际读取区间却是[offset ~ endOfCurBlock],导致[readEnd ~ endOfCurBlock]这部分数据做了无效读取。这样设计主要是为了优化顺序读场景,通过预读来加快后续访问,然而针对随机读场景(小数据量下比较普遍),该方式却适得其反,因为预读出的数据很难被后续使用,增加了读放大行为。我们优化为客户端只向DN传递需要读取的数据区间,DN侧不提前预取,只返回客户端需要的数据。

图11:读放大过程示意图

  1. DN慢节点导致作业运行时间变长。我们发现很多大作业的HDFS长尾耗时非常高,HDFS的平均read时延只有10ms左右,P99.99时延却达到了6秒,耗时最长的请求甚至达到了5分钟,但在不启用EC场景下,HDFS的每个block会有三副本,完全可以切换到空闲DN访问。为此我们对客户端的读请求链路做了重新的设计与调整,实时监测每个DN的负载情况,基于P99.9分位请求时延判定慢节点,并将读请求路由到负载较低的DN上面。

HDFS Native客户端读优化之后,平均读写延迟降低了2/3,吞吐提升2倍。

4.4 Shuffle重构

Gluten在shuffle策略的支持上,没有预留好接口,每新增一种shuffle模式需要较大改动。美团有自研的Shuffle Service,其他公司也可能有自己的Shuffle Service(如Celeborn),为了更好适配多种shuffle模式,我们提议对shuffle接口重新梳理,并主导了此讨论和设计。

Gluten中的shuffle抽象第一层是数据格式(Velox是RowVector,Gluten引入的Arrow是RecordBatch),第二层是分区方式(RoundRobin、SinglePart、Hash、Range),如果要支持新shuffle模式(local、remote),需要实现2*4*2=16个writer,将会有大量冗余代码。分区具体实现应该与数据格式和shuffle模式无关,我们用组合模式替代继承模式。另外,我们在shuffle中直接支持了RowVector,避免Velox和Arrow对应数据类型之间的额外转换开销。

图12:重构前后shuffle模块UML对比

4.5 适配HBO

HBO(Historical Based Optimization)是通过作业历史运行过程中资源的实际使用量,来预测作业下一次运行需要的资源并设置资源相关参数的一种优化手段。美团过去在原生Spark上通过调配堆内内存取得了8%左右的内存资源节省。

Gluten主要使用堆外内存(off-heap),这与原生Spark主要使用堆内内存(on-heap)不同。初期出于稳定性考虑Gluten和原生Spark的运行参数整体一致,总内存大小相同,Gluten off-heap 占比75%, on-heap占比25%。这样配置既会导致内存利用率不高(原生Spark的内存使用率58%,向量化版作业内存使用率 38%),也会使一部分作业on-heap内存配置偏低,在算子回退时导致任务OOM。

我们把HBO策略推广到堆外内存,向量化计算的内存节省比例从30%提升到40%,由于heap内存配置不合理的OOM问题全部消除。

图13:HBO流程图

4.6 一致性问题

  1. 低版本ORC数据丢失。hive-0.13之前使用的ORC,Footer信息不包含列名,只有ID用来表示第几列(如Col1, Col2…)。Velox TableScan算子在扫表的时候,如果下推的Filter里包含IsNotNull(A),会根据列名A查找该列数据,由于无法匹配到列名,会误判空文件,导致数据缺失。Spark在生成读ORC表的执行计划时,通过访问HiveMetaStore得到表的Schema信息,并在物理算子FileSourceScanExec中保存了表的Schema信息。Gluten对该算子进行doTransform()转换时,会把表的Schema信息序列化到Substrait的ReadRel里。接下来的Substrait计划转Velox计划阶段,我们把表的Schema信息传给Velox的HiveTableHandle,在构造Velox的DwrfReader时修正ORC文件Footer里的Schema信息(如果Footer的Schema不包含列名,就读取表Schema里的对应列的名称进行赋值),解决了这个问题。

  2. count distinct结果错误。比如这样一条SQL:select A, B, count(distinct userId), sum(amt) from t group by 1,2 ,Gluten会把count(distinct userId) 变为count(userId),通过把userId加到GroupingKey里来实现distinct语义。具体处理过程如下:

表1:示例SQL在Spark中的处理步骤

在第3步的Intermediate Aggregation中,为了节省内存和加速执行,当Velox的HashAggregate算子满足触发Flush的条件时(HashTable内存占用超过阈值或者聚合效果低于阈值),Velox会标记 partialFull=true,触发Flush操作(计算HashTable里已经缓存数据的Intermediate Result),并且后续输入的数据不再执行Intermediate Aggregation的计算,直接进入第4步的Partial Aggregation。如果后续输入的数据里包含重复的userId,count(userId)会因为去重不彻底而结果错误。我们短期的修复方案是禁用Intermediate Aggregation的提前Flush功能,直到所有数据都输入之后再获取该阶段的聚合结果。

这个方案的弊端有两个:1)HashTable的内存占用会变大,需要同时开启HashAggregate算子的Spill功能避免OOM;2)直接修改了Velox的HashAggregate算子内部代码,从Velox自身的角度来看,没有单独针对Distinct相关的聚合做处理,随着后续迭代,可能影响所有用到Intermediate Aggregation的聚合过程。

鉴于此,Gluten社区提供了一个更加均衡的解决方案,针对这类Distinct Aggregation,生成执行计划时,Spark的Partial Merge Aggregation不再生成Intermediate Aggregation,改为生成Final Aggregation(不会提前flush、不使用merge_sum),同时配合聚合函数的Partial Companion函数来返回Intermediate结果。这样就从执行计划转换策略层面规避这个问题,避免对Velox里Final Aggregation内部代码做过多的改动。

  1. 浮点类型转换精度错误。形如查询SELECT concat(col2, cast(max(col1) as string)) FROM (VALUES (CAST(5.08 AS FLOAT), 'abc_')) AS tab(col1, col2) group by col2; 在Spark中返回abc_5.08,在Gluten中返回abc_5.079999923706055。浮点数5.08不能用二进制分数精确表达,近似表示成5.0799999237060546875。Velox通过函数folly::to<std::string>(T val)来实现float类型到string类型的转换,这个函数底层依赖开源库google::double-conversion, folly里默认设置了输出宽度参数DoubleToStringConverter::SHORTEST(可以准确表示double类型的最小宽度),转换时经过四舍五入之后返回 5.079999923706055。我们把宽度参数改为DoubleToStringConverter::SHORTEST_SINGLE(可以准确表示float类型的最小宽度),转换时经过四舍五入之后返回 5.08。

5 上线效果

我们已上线了2万多ETL作业,平均内存资源节省40%+,平均执行时间减少13%,证明Gluten+Velox的向量化计算方案生产可行。向量化计算除了能提高计算效率,也能提高读写数据的效率,如某个作业的Input数据有30TB,过去需要执行7小时,绝大部份时间花在了读数据和解压缩上面。使用向量化引擎后,因为上文提到的ISA-L解压缩优化,列转行的开销节省,以及HDFS Native客户端优化,执行时间减少到2小时内。

图14:上线优化效果

6 未来规划

我们已上线向量化计算的Spark任务只是小部分,计划2024年能让绝大部分的SQL任务运行在向量化引擎上。

6.1 Spark向量化之后对开源社区的跟进策略

Spark、Gluten、Velox三个社区各有自己考虑和版本发布节奏,从一个社区到多个社区的引擎维护复杂度上升。我们的应对有二,一是计算引擎有不同层次,Spark升级主要考虑功能语义实现、执行计划、资源和task调度,Gluten和Velox的升级主要考虑物理算子性能优化,各取所长;二是尽量减少和社区的差异,公司内部适配只在Spark中实现,公司内的UDF以git submodule形式单独维护。

  1. 升级到Spark3.5。Gluten最低支持的Spark版本为3.2,23年我们为了降低验证成本,选择在Spark3.0兼容Gluten,但继续升级迭代成本比较高,在推广之前,应该升级到更新的Spark版本。Spark3.5将会是Gluten社区对Spark3.x上长期支持的稳定版本。高版本Spark也有一些额外收益,我们基于TPC-H实测,Spark3.5相比Spark3.0,「memory*second」减少40%,执行时间减少17%,根据之前升级经验,生产环境大约能达到一半效果。
  2. 保持Spark版本长期稳定。高版本Spark对Hadoop版本的升级迭代带来比较高适配成本,内部迭代的feature也有比较高的迁移成本,因此我们平均3年才会升级一次Spark版本,更多是将需要的feature合并到内部分支。
  3. 快速跟进Gluten/Velox新版本。升级到Spark3.5之后,我们内部Spark版本与Gluten社区的兼容性成本很低,并且向量化相关feature还会持续迭代,预计每半年可升级一次线上版本。

6.2 提升向量化覆盖率的策略

  1. 扩大向量化算子和UDF范围。我们整理了影响权重最高的几十个算子回退问题与Gluten社区一起解决,对于大量内部UDF,则会探索用大模型来将UDF批量改写为C++版本的向量化实现。

  2. 扩大File format支持向量化范围。美团内部有约20%的表为textfile格式,还有接近10%的表使用内部开发的format,只能按行读取也不支持下推,加上行转列都会有额外性能开销,影响最终效果。我们将会把textfile全部转为ORC,为自研format提供C++客户端,进一步提升向量化计算性能。

7 致谢

感谢Intel Gluten合作伙伴高明、周渊、宾伟、韦廷、宏泽、莫芮、飞龙、马榕、镇辉、成成等的大力支持和辛勤付出,也感谢Gluten和Velox社区贡献者的开源精神和无私奉献。

8 本文作者

luhao、左军、lux、kecookier、cx14、陈皮兔、dju,均来自美团基础研发平台。

CppCon 2023 Implementing Coroutines Using C++17 -- Alon Wolf

作者 Blog Staff
2024年6月23日 02:09

cpp23-wolf.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

Lightning Talk: Implementing Coroutines Using C++17

by Alon Wolf 

Summary of the talk:

In this lightning talk, we will explore the journey of implementing coroutines in C++17 before they were added to the language in C++20.

The implementation uses macros, template metaprogramming, assembly functions, and more that resulting in working coroutines despite somewhat "horrible" code.

Discover how local variables within the coroutine body were leveraged to calculate frame sizes and ensure correct variable lifetimes during suspension, resumption, and destruction.

使用 PipeWire 实现自动应用均衡器

作者 依云
2024年6月22日 15:51

本文来自依云's Blog,转载请注明。

之前我写过一篇文章,讲述我使用 EasyEffects 的均衡器来调整 Bose 音箱的音效。最近读者 RNE 留言说可以直接通过 PipeWire 实现,于是前几天我实现了一下。

先说一下换了之后的体验。相比于 EasyEffects,使用 PipeWire 实现此功能获得了以下好处:

  • 少用一个软件(虽然并没有多大)。
  • 不依赖图形界面。EasyEffects 没有图形界面是启动不了的。
  • 占用内存少。EasyEffects 有时候会占用很多内存,不知道是什么问题。
  • 自己实现的切换逻辑,更符合自己的需求。EasyEffects 只能针对指定设备加载指定的配置,不能指定未知设备加载什么配置。因此,当我的内置扬声器名称在「alsa_output.pci-0000_00_1f.3.analog-stereo」、「alsa_output.pci-0000_00_1f.3.7.analog-stereo」或者「alsa_output.pci-0000_00_1f.3.13.analog-stereo」等之间变化时,我需要一个个名称地指定要加载的配置。
  • 只要打开 pavucontrol 就能确认均衡器是否被应用了。EasyEffects 需要按两下Shift-Tab和空格(或者找找鼠标)来切换界面。

缺点嘛,就是我偶尔使用的「自动增益」功能没啦。不过自动增益的效果并不太好,我都是手动按需开关的。没了就没了吧。

配置方法

首先要定义均衡器。创建「~/.config/pipewire/pipewire.conf.d/bose-eq.conf」文件,按《Linux好声音(干净的均衡器)》一文的方式把均衡器定义写进去就好了。我的文件见 GitHub

然后需要在合适的时候使用这个均衡器。实际上上述配置加载之后,PipeWire 里就会多出来一对名叫「Bose Equalizer Sink」的设备,一个 source 一个 sink。把 source 接到音箱,播放声音的程序接到 sink,就用上了。别问我为什么 source 的名字也是「Sink」,我不会分开定义它们的名字……

自动化应用使用的是 WirePlumber 脚本。它应该放在「~/.local/share/wireplumber/scripts」里,但是我为了方便放到 dotconfig 仓库里管理,在这里放了个到「~/.config/wireplumber/scripts」的软链接。脚本干的事情很简单:在选择输出设备的时候,看看当前默认设备是不是 Bose 音箱;如果是,就选择之前定义的「Bose Equalizer Sink」作为输出目标。不过因为文档匮乏,为了干成这件事花了我不少精力,翻看了不少 WirePlumber 自带脚本和源码。最终的脚本也在 GitHub 上

结语

PipeWire 挺强大的,就是文档太「瘦弱」啦。能用脚本配置的软件都很棒~

再次感谢读者 RNE 的留言~

Pulling a Single Item From a C++ Parameter Pack by its Index -- Raymond Chen

作者 Blog Staff
2024年6月22日 05:22

RaymondChen_5in-150x150.jpgThis article explores techniques to access specific elements within a C++ parameter pack by index. It delves into the use of std::tie for creating a tuple of lvalue references and explains how std::forward_as_tuple can preserve the original reference categories of the parameters. Additionally, it highlights a proposed feature in C++26, Pack Indexing, which aims to simplify this process significantly.

Pulling a Single Item From a C++ Parameter Pack by its Index

by Raymond Chen

From the article:

Suppose you have a C++ parameter pack and you want to pluck out an item from it by index.

template<int index, typename...Args>
void example(Args&&... args)
{
    // how do I access the index'th args parameter?
}

One solution is to use std::tie:

template<int index, typename...Args>
void example(Args&&... args)
{
    auto& arg = std::get<index>(
        std::tie(args...));
}

CppCon 2023 C++ Memory Model: from C++11 to C++23 -- Alex Dathskovsky

作者 Blog Staff
2024年6月21日 02:05

cpp23-dathskovsky.pngRegistration is now open for CppCon 2024! The conference starts on September 15 and will be held in person in Aurora, CO. To whet your appetite for this year’s conference, we’re posting videos of some of the top-rated talks from last year's conference. Here’s another CppCon talk video we hope you will enjoy – and why not register today for CppCon 2024!

C++ Memory Model: from C++11 to C++23

by Alex Dathskovsky

Summary of the talk:

In the realm of C++ development, threading and memory management play a crucial role in crafting highly parallel and optimized programs. However, the absence of a memory model in C++98 posed challenges. Thankfully, with the advent of C++11, significant changes were introduced, including the introduction of a memory model, which brought forth a plethora of new and exciting tools for developers to leverage. This talk aims to delve into the realm of the C++ memory model, showcasing the arsenal of tools at our disposal. Attendees will gain insights into how CPUs and compilers optimize code and understand the criticality of adhering to the memory model correctly. Practical guidelines on utilizing these tools effectively will also be explored.

Throughout the talk, we will illustrate practical examples and share best practices for utilizing the diverse set of tools now available to us. From atomic operations to memory barriers, we will explore the range of techniques that allow us to develop robust and thread-safe code.

This talk will also illustrate the newer tools from newer C++ standards like JThread and so this talk will show how memory model is used and how it advanced since C++11.

❌
❌