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