• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# TS&JS高性能编程实践及使用工具的指导
2
3## 概述
4
5本文参考业界标准,并结合应用TS&JS部分的性能优化实践经验,从应用编程指南、高性能编程实践、性能优化调试工具等维度,为应用开发者提供参考指导,助力开发者开发出高性能的应用。
6
7本文主要提供高性能编程实践的相关建议,应用开发的具体编程指导可见[《OpenHarmony应用TS&JS编程指南》](../../contribute/OpenHarmony-Application-Typescript-JavaScript-coding-guide.md)。
8
9## 应用TS&JS高性能编程实践
10
11高性能编程实践,是在开发过程中逐步总结出来的一些高性能的写法和建议,在业务功能实现过程中,我们要同步思考并理解高性能写法的原理,运用到代码逻辑实现中。
12
13本文中的实践示例代码,会统一标注正例或者反例,正例为推荐写法,反例为不推荐写法。
14
15### 属性访问与属性增删
16
17#### 热点循环中常量提取,减少属性访问次数
18
19在实际的应用场景中抽离出来如下用例,其在循环中会大量进行一些常量的访问操作,该常量在循环中不会改变,可以提取到循环外部,减少属性访问的次数。
20
21【反例】
22
23``` TypeScript
24// 优化前代码
25private getDay(year: number): number {
26  /* Year has (12 * 29 =) 348 days at least */
27  let totalDays: number = 348;
28  for (let index: number = 0x8000; index > 0x8; index >>= 1) {
29    // 此处会多次对Time的INFO及START进行查找,并且每次查找出来的值是相同的
30    totalDays += ((Time.INFO[year- Time.START] & index) !== 0) ? 1 : 0;
31  }
32  return totalDays + this.getDays(year);
33}
34```
35
36可以将`Time.INFO[year - Time.START]`进行热点函数常量提取操作,这样可以大幅减少属性的访问次数,性能收益明显。
37
38【正例】
39
40``` TypeScript
41// 优化后代码
42private getDay(year: number): number {
43  /* Year has (12 * 29 =) 348 days at least */
44  let totalDays: number = 348;
45  const info = Time.INFO[year - Time.START]; // 1. 从循环中提取不变量
46  for (let index: number = 0x8000; index > 0x8; index >>= 1) {
47    if ((info & index) !== 0) {
48      totalDays++;
49    }
50  }
51  return totalDays + this.getDays(year);
52}
53```
54
55#### 避免频繁使用delete
56
57delete对象的某一个属性会改变其布局,影响运行时优化效果,导致执行性能下降。
58
59> **说明:**
60>
61> 不建议直接使用delete删除对象的任何属性,如果有需要,建议使用map和set或者引擎实现的[高性能容器类](../arkts-utils/container-overview.md)。
62
63【反例】
64
65``` TypeScript
66class O1 {
67  x: string | undefined = "";
68  y: string | undefined = "";
69}
70let obj: O1 = {x: "", y: ""};
71
72obj.x = "xxx";
73obj.y = "yyy";
74delete obj.x;
75```
76
77建议使用如下两种写法之一实现属性的增删。
78
79【正例】
80
81``` TypeScript
82// 例1:将Object中不再使用的属性设置为null
83class O1 {
84  x: string | null = "";
85  y: string | null = "";
86}
87let obj: O1 = {x: "", y: ""};
88
89obj.x = "xxx";
90obj.y = "yyy";
91obj.x = null;
92
93// 例2:使用高性能容器类操作属性
94import HashMap from '@ohos.util.HashMap';
95let myMap= new HashMap();
96
97myMap.set("x", "xxx");
98myMap.set("y", "yyy");
99myMap.remove("x");
100```
101
102### 数值计算
103
104#### 数值计算避免溢出
105
106常见的可能导致溢出的数值计算包括如下场景,溢出之后,会导致引擎走入慢速的溢出逻辑分支处理,影响后续的性能。
107
108- 针对加法、减法、乘法、指数运算等运算操作,应避免数值大于INT32_MAX或小于INT32_MIN,否则会导致int溢出。
109
110- 针对&(and)、>>>(无符号右移)等运算操作,应避免数值大于INT32_MAX,否则会导致int溢出。
111
112### 数据结构
113
114#### 使用合适的数据结构
115
116在实际的应用场景中抽离出来如下用例,该接口中使用JS Object来作为容器去处理Map的逻辑,建议使用HashMap来进行处理。
117
118【反例】
119
120``` TypeScript
121getInfo(t1, t2) {
122  if (!this.check(t1, t2)) {
123    return "";
124  }
125  // 此处使用JS Object作为容器
126  let info= {};
127  this.setInfo(info);
128  let t1= info[t2];
129  return (t1!= null) ? t1: "";
130}
131setInfo(info) {
132  // 接口内部实际上进行的是map的操作
133  info[T1] = '七六';
134  info[T2] = '九一';
135  ... ...
136  info[T3] = '十二';
137}
138```
139
140代码可以进行如下修改,除了使用引擎中提供的标准内置map之外,还可以使用ArkTS提供的[高性能容器类](../arkts-utils/container-overview.md)。
141
142【正例】
143
144``` TypeScript
145import HashMap from '@ohos.util.HashMap';
146
147getInfo(t1, t2) {
148  if (!this.check(t1, t2)) {
149    return "";
150  }
151  // 此处替换为HashMap作为容器
152  let info= new HashMap();
153  this.setInfo(info);
154  let t1= info.get(t2);
155  return (t1!= null) ? t1: "";
156}
157setInfo(info) {
158  // 接口内部实际上进行的是map的操作
159  info.set(T1, '七六');
160  info.set(T2, '九一');
161  ... ...
162  info.set(T3, '十二');
163}
164```
165
166#### 数值数组推荐使用TypedArray
167
168如果是涉及纯数值计算的场合,推荐使用TypedArray数据结构。
169
170常见的TypedArray包括:Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array。
171
172【正例】
173
174``` TypeScript
175const typedArray1 = new Int8Array([1, 2, 3]);  // 针对这一场景,建议不要使用new Array([1, 2, 3])
176const typedArray2 = new Int8Array([4, 5, 6]);  // 针对这一场景,建议不要使用new Array([4, 5, 6])
177let res = new Int8Array(3);
178for (let i = 0; i < 3; i++) {
179  res[i] = typedArray1[i] + typedArray2[i];
180}
181```
182
183#### 避免使用稀疏数组
184
185分配数组时,应避免其大小超过1024或形成稀疏数组。
186
187虚拟机在分配超过1024大小的数组或者针对稀疏数组,均采用hash表来存储元素,相对使用偏移来访问数组元素速度较慢。
188
189在开发时,尽量避免数组变成稀疏数组。
190
191【反例】
192
193``` TypeScript
194// 如下几种情形会变成稀疏数组
195// 1. 直接分配100000大小的数组,虚拟机会处理成用hash表来存储元素
196let count = 100000;
197let result: number[] = new Array(count);
198
199// 2. 分配数组之后直接,在9999处初始化,会变成稀疏数组
200let result: number[] = new Array();
201result[9999] = 0;
202
203// 3. 删除数组的element属性,虚拟机也会处理成用hash表来存储元素
204let result = [0, 1, 2, 3, 4];
205delete result[0];
206```
207
208### 对象初始化
209
210#### 使用字面量进行对象创建
211
212通常在代码中,进行一些对象创建的时候,大家会采用动态添加属性方式,这种方式,在前端解析时,不能获取到更多的信息,因此不能为运行时提供优化信息。
213
214【反例】
215
216``` TypeScript
217let arr = new Array();  // 创建一个array
218
219let obj = new Object();  // 创建一个普通对象
220
221let oFruit = new Object();
222oFruit.color = "red";
223oFruit.name = "apple"; // 创建一个对象,并设置属性
224```
225
226在要求性能的场合,可以使用字面量进行对象创建,这样在运行时可以获得指令级别的优化。
227
228【正例】
229
230``` TypeScript
231let arr = [];  // 创建一个array
232
233let obj = {}; // 创建一个普通对象
234
235class O1 {
236  color: string = "";
237  name: string = "";
238}
239let oFruit: O1 = {color: "red", name: "apple"};  // 创建一个对象,并设置属性
240```
241
242#### 对象构造初始化
243
244对象构造的时候,要提供默认值初始化,不要访问未初始化的属性。
245
246【反例】
247
248``` TypeScript
249// 不要访问未初始化的属性
250class A {
251  x: number;
252}
253
254// 构造函数中要对属性进行初始化
255class A {
256  x: number;
257  constructor() {
258  }
259}
260
261let a = new A();
262// x使用时还未赋值,这种情况会访问整个原型链
263print(a.x);
264```
265
266【正例】
267
268``` TypeScript
269// 推荐一:声明初始化
270class A {
271  x: number = 0;
272}
273
274// 推荐二:构造函数直接赋初值
275class A {
276  constructor() {
277    this.x = 0;
278  }
279}
280
281let a = new A();
282print(a.x);
283```
284
285#### number正确初始化
286
287针对number类型,编译器在优化时会区分整型和浮点类型。开发者在初始化时如果预期是整型就初始化成0,如果预期是浮点型就初始化为0.0,不要把一个number类型初始化成undefined或者null。
288
289【正例】
290
291``` TypeScript
292function foo(d: number) : number {
293  // 变量i预期是整型,不要声明成undefined/null或0.0,直接初始化为0
294  let i: number = 0;
295  i += d;
296  return i;
297}
298```
299
300#### 避免动态添加属性
301
302对象在创建的时候,如果开发者明确后续还需要添加属性,可以提前置为undefined。动态添加属性会导致对象布局变化,影响编译器和运行时优化效果。
303
304【反例】
305
306``` TypeScript
307// 后续obj需要再添加z属性
308class O1 {
309  x: string = "";
310  y: string = "";
311}
312let obj: O1 = {"x": xxx, "y": "yyy"};
313...
314// 这种动态添加方式是不推荐的
315obj.z = "zzz";
316```
317
318【正例】
319
320``` TypeScript
321class O1 {
322  x: string = "";
323  y: string = "";
324  z: string = "";
325}
326let obj: O1 = {"x": "xxx", "y": "yyy", "z": ""};
327...
328obj.z = "zzz";
329```
330
331#### 调用构造函数的入参要与标注类型匹配
332
333由于TS语言类型系统是一种标注类型,不是编译期强制约束,如果入参的实际类型与标注类型不匹配,会影响引擎内部的优化效果。
334
335【反例】
336
337``` TypeScript
338class A {
339    private a: number | undefined;
340    private b: number | undefined;
341    private c: number | undefined;
342    constructor(a?: number, b?: number, c?: number) {
343        this.a = a;
344        this.b = b;
345        this.c = c;
346    }
347}
348// new的过程中没有传入参数,a,b,c会获取一个undefined的初值,和标注类型不符
349let a = new A();
350```
351
352针对上文的示例场景,开发者大概率预期该入参类型是number类型,需要显式写出来。
353
354参照正例进行如下修改,不然会造成标注的入参是number,实际传入的是undefined。
355
356【正例】
357
358``` TypeScript
359class A {
360    private a: number | undefined;
361    private b: number | undefined;
362    private c: number | undefined;
363    constructor(a?: number, b?: number, c?: number) {
364        this.a = a;
365        this.b = b;
366        this.c = c;
367    }
368}
369// 初始化直接传入默认值0
370let a = new A(0, 0, 0);
371```
372
373#### 不变的变量声明为const
374
375不变的变量推荐使用const进行初始化。
376
377【反例】
378
379``` TypeScript
380// 该变量在后续过程中并未发生更改,建议声明为常量
381let N = 10000;
382
383function getN() {
384  return N;
385}
386```
387
388【正例】
389
390``` TypeScript
391const N = 10000;
392
393function getN() {
394  return N;
395}
396```
397
398### 接口及继承
399
400#### 避免使用type类型标注
401
402如果传入的参数类型是type类型,实际入参可能是一个object literal,也可能是一个class,编译器及虚拟机因为类型不固定,无法做编译期假设进而进行相应的优化。
403
404【反例】
405
406``` TypeScript
407// type类型无法在编译期确认, 可能是一个object literal,也可能是另一个class Person
408type Person = {
409  name: string;
410  age: number;
411};
412
413function greet(person: Person) {
414  return "Hello " + person.name;
415}
416
417// type方式是不推荐的,因为其有如下两种使用方式,type类型无法在编译期确认
418// 调用方式一
419class O1 {
420  name: string = "";
421  age: number = 0;
422}
423let objectliteral: O1 = {name : "zhangsan", age: 20 };
424greet(objectliteral);
425
426// 调用方式二
427class Person {
428  name: string = "zhangsan";
429  age: number = 20;
430}
431let person = new Person();
432greet(person);
433```
434
435【正例】
436
437``` TypeScript
438interface Person {
439  name: string ;
440  age: number;
441}
442
443function greet(person: Person) {
444  return "Hello " + person.name;
445}
446
447class Person {
448  name: string = "zhangsan";
449  age: number = 20;
450}
451
452let person = new Person();
453greet(person);
454```
455
456### 函数调用
457
458#### 声明参数要和实际的参数一致
459
460声明的参数要和实际的传入参数个数及类型一致,如果不传入参数,则会作为undefined处理,可能造成与实际入参类型不匹配的情况,从而导致运行时走入慢速路径,影响性能。
461
462【反例】
463
464``` TypeScript
465function add(a: number, b: number) {
466  return a + b;
467}
468// 参数个数是2,不能给3个
469add(1, 2, 3);
470// 参数个数是2,不能给1个
471add(1);
472// 参数类型是number,不能给string
473add("hello", "world");
474```
475
476【正例】
477
478``` TypeScript
479function add(a: number, b: number) {
480  return a + b;
481}
482// 按照函数参数个数及类型要求传入参数
483add(1, 2);
484```
485
486#### 函数内部变量尽量使用参数传递
487
488能传递参数的尽量传递参数,不要使用闭包。闭包作为参数会多一次闭包创建和访问。
489
490【反例】
491
492``` TypeScript
493let arr = [0, 1, 2];
494
495function foo() {
496  // arr 尽量通过参数传递
497  return arr[0] + arr[1];
498}
499foo();
500```
501
502【正例】
503
504``` TypeScript
505let arr = [0, 1, 2];
506
507function foo(array: Array) : number {
508  // arr 尽量通过参数传递
509  return array[0] + array[1];
510}
511foo(arr);
512```
513
514### 函数与类声明
515
516#### 避免动态声明function与class
517
518不建议动态声明function和class。
519
520以如下用例为例,动态声明了class Add和class Sub,每次调用`foo`都会重新创建class Add和class Sub,对内存和性能都会有影响。
521
522【反例】
523
524``` TypeScript
525function foo(f: boolean) {
526  if (f) {
527    return class Add{};
528  } else {
529    return class Sub{};
530  }
531}
532```
533
534【正例】
535
536``` TypeScript
537class Add{};
538class Sub{};
539function foo(f: boolean) {
540  if (f) {
541    return Add;
542  } else {
543    return Sub;
544  }
545}
546```
547
548## TS&JS性能优化工具使用
549
550通过如下工具和使用方法,能够帮助开发者查看待分析场景下各阶段的耗时分布情况,并进一步针对耗时情况使用对应的工具做细化分析。
551
552工具使用介绍:
553
5541. 针对应用开发者,推荐使用自带的[Smartperf工具](../../device-dev/device-test/smartperf-host.md)来进行辅助分析,可以从宏观角度查看应用各个阶段耗时分布情况,快速找到待分析优化模块。
5552. 针对第一步分析得到的待优化模块,需要进行进一步分析确认耗时点是在TS&JS部分还是C++部分。C++部分耗时模块细化分析建议使用hiperf工具;针对TS&JS部分耗时,可以使用[CPU Profiler工具](application-performance-analysis.md)。
5563. 针对虚拟机开发者,如果需要进一步拆分细化,推荐使用虚拟机提供的RUNTIME_STAT工具。
557
558### Smartperf工具使用指导
559
560以如下某个应用场景使用过程的trace为例,可以通过[Smartperf工具](../../device-dev/device-test/smartperf-host.md)抓取到应用使用阶段的耗时信息,其中大部分为GC(Garbage Collection,垃圾回收)等操作。如果此接口大部分是应用开发者通过TS&JS实现,并且在trace中体现此阶段比较耗时,则可以继续使用[CPU Profiler工具](application-performance-analysis.md)来进一步分析TS&JS部分耗时情况。
561
562除了可以查看系统的trace之外,还可以在应用的源码的关键流程中加入一些trace点,用于做性能分析。startTrace用于记录trace起点,finishTrace用于记录trace终点,在应用中增加trace点的方式如下:
563
564``` TypeScript
565import hiTraceMeter from '@ohos.hiTraceMeter';
566... ...
567hiTraceMeter.startTrace("fillText1", 100);
568... ...
569hiTraceMeter.finishTrace("fillText1", 100);
570```
571
572在应用层或Native层增加trace点,具体可见 [性能打点跟踪开发指导](../dfx/hitracemeter-guidelines.md)。
573
574### hiperf工具使用指导
575
576集成在Smartperf的hiperf工具使用指导,具体可见 [HiPerf的抓取和展示说明](https://gitee.com/openharmony/developtools_smartperf_host/blob/master/ide/src/doc/md/quickstart_hiperf.md)577
578hiperf工具的单独使用指导,具体可见 [hiperf应用性能优化工具](https://gitee.com/openharmony/developtools_hiperf)579
580### TS&JS及NAPI层面耗时分析工具
581
582TS&JS层面耗时主要分为如下几种情况:
583
5841. Ability的生命周期回调的耗时。
585
5862. 组件的TS&JS业务代码的回调的耗时。
587
5883. 应用TS&JS逻辑代码耗时。
589
590NAPI层面的耗时主要分为如下几种情况:
591
5921. TS&JS业务代码通过调用JS API产生的耗时。
593
5942. TS&JS业务代码调用开发者通过NAPI封装的C/C++实现时产生的耗时。
595
596针对应用中的TS&JS及NAPI两种业务场景的耗时分析,我们提供了[CPU Profiler工具](application-performance-analysis.md),用来识别热点函数及耗时代码。
597
598其支持的采集方式如下:
599
600- DevEco Studio连接设备实时采集;
601
602- hdc shell连接设备进行命令行采集。
603
604可以通过CPU Profiler工具,对TS&JS中执行的热点函数进行抓取。以应用实际使用场景为例,在此场景中,可以抓到应用中的某一热点函数,在此基础上,针对该接口做进一步分析。
605