【冷知识】为何要用 String.reserve( )

文章目录


https://www.arduino.cn/thread-12558-1-1.html
String 是 C++ 的类别(class), 它比 C 语言原生的 char Array 字符串好用,
但相对的当然比直接用 C 语言原生字符串慢非常多 !
那可否让 C++ String 的运作快一点呢 ?

常常看到范例程序中写:

    String gg="";

然后在 void setup( ) { 内写:
gg.reserve(200); // 保留 200 char 位置
参考:
http://arduino.cc/en/Reference/StringReserve

Q: 这样写到底有啥小路用呢 ?

A: 先说答案:
这会让你的程序做 gg += (char)yy; 的时候会快一点 !

Q: 至于像 gg += (char)yy; 是在何时会这样写呢?

A: 通常你是当 Serial.available( ) 之时做类似:

     int yy = Serial.read( );
     if( yy != -1) gg += (char)yy;

或是会这样写:

    gg = "";
    while( Serial.available( ) ) {
      gg += (char)yy;
      delay(2);
    } // while(

Q: 那为何写了 gg.reserve(200); 就会使得 gg += (char)yy; 比较快呢?
A: 这是因为 String 类别实作方式的关系!
因为 String 其实是 C++ 的类别, 它不是 C 的字符串,
很好用, 但相对比 C 的字符串 char haha[ ] = “hello you!”; 慢很多!
在 String class 内部,
当你写 String gg="";
它只帮 gg 安排一个 byte 的位置,
每当你要做 gg += (char)yy; 或 gg+= “you”; 或 gg+= 任何数; 之时,
它会做:
(1)先检查 += 右边的, 如果是 int 就先转换成字符串;
如果是 float 或 double,
要弄个临时字符串 = 整数部分 + “.” + 小数点后取两位(从小数点后第三位四舍五入)
(2)算出在(1)的临时字符串的长度
注意, 这时的临时字符串还是 C 的字符串, 不是 C++ 的 String
算长度只能一个 char 一个 char 算, 直到遇见代表字符串结束的 0 才知道!
(3)取出 gg 的长度(C++ 的 String 有记住字符串长度),
其实 C++ 的 String 至少记住三样数据 :
(a)指针(pointer;指标)指向一个 char Array,
此 Array 是用 C 语言存放字符串方式储存字符串!
(b)该 char Array 的空间大小 (Capacity)
©已经真正用掉几个 char, 这就是字符串的长度 length
(4)看看如果把右边字符串加上去之后原先 gg 的空间 capacity 够不够?
如果位置够用, 到(5); 如果位置不够用, 做:
(a)先把 gg 的位置(就是在(3)(a)说的指针)用临时变量记住
(b)算出 len = gg 原先字符串长度 + 右边临时字符串长度
©去要(malloc)一块连续 len 个 char 的内存, 把 gg 的指针指向该新要来的位置
(d)把旧的字符串全部复制到新的位置(调用 C 语言的 strcpy()函数)
(e)把在(a)临时记住原先 gg 的指针指过去的内存全部还给系统
(f)更新 gg 的 capacity 与 length
(5)把右边要加的字符串加入到 gg 字符串的尾巴!
这动作是依据 gg 的指针指过去的地方加上原先的 length,
调用 strcpy( ) 把右边临时字符串复制到该处开始的地方,
更新 gg 的 length
(6)请注意, 这时 gg 把内存用得刚刚好,
也就是说这时gg的 capacity 刚好等于 length,
这是个严重的缺点, 因为下次再做 gg += (char)yy;
即使只是增加一个 char, 又要重复刚刚说的(1)到(5)的工作 !!
随着 gg 字符串不断的增长, 要了又还掉的旧位置会变成不连续的"破洞",
如果内存(memory)已经不够用, 可能会要不到给 gg 用的新位置,
目前 Arduino 版本的 String 这时只是不管你, 不会发处错误信息 !

看到没, 够多事情要做吧 !?
但是…
如果你在 setup( ) 内事先做了 gg.reserve(200); 则它会先去要一块内存,
可以放 200+1 = 201 char 的位置(别忘了最后要放整数 0 表示字符串结束!)
那么以后, 只要你的字符串 gg 的总长度没超过 200个char,
就永远用旧的位置, 不会做要新位置又复制等繁杂的工作 !

请注意, 写 gg = “abcdef”; 这样也是可能需要去跟系统要新位置并把旧的位置占用的内存还掉 !
就是原先的 capacity 容量不够放得下等号右边的字符串之时就必须做这些复杂的动作 !!

关于 String class 的源代码可参考:
https://github.com/arduino/Ardui … no/WString.cpp#L145

或是在你的 Arduino IDE 目录下的 hardware/arduino/cores/arduino/WString.cpp

// 补充一下
//String operation is SLOW
//想要知道 String 的运作有多慢,
//可以写个简单程序来测试:
//以下范例共测试三种运算, 各做 200次(当然 for LOOP 本身也会浪费时间!)
// test( ) 测试做 int 加法
// test22( ) 做 String gg += 一个 char
// test33( ) 也是做 String yy += 一个 char, 但有事先 yy.reserve(228);
//事实上如果只是做整数 int 加法,
//在 for Loop 和 if 用掉的时间远大于 ggyy 做 int 加法!

String gg = "";
String yy = "";
int ggyy = 0;
unsigned long begt, endt, runt;
void setup( ) {
  Serial.begin(9600);
  yy.reserve(228);
  test( );
  test22( );
  test33( );
}
void loop( ) {
  ;
}
void test( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) ggyy += '\n';
    else ggyy += 'A';
  }
  endt = micros( );
  Serial.println(String("ggyy is int, Run time = ") + (endt - begt) + "us");
  Serial.println(String("ggyy now = ") + ggyy + "\r\n");
  Serial.flush( ); delay(258);
} // test(
void test22( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) gg += '\n';
    else gg += 'A';
  }
  endt = micros( );
  Serial.println(String("gg No reserve, Run time = ") + (endt - begt) + "us");
  Serial.println(gg + "\r\n");
  Serial.flush( ); delay(258);
}
void test33( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) yy += '\n';
    else yy += 'B';
  }
  endt = micros( );
  Serial.println(String("yy.reserve(228), Run time = ") + (endt - begt) + "us");
  Serial.println(yy);
  Serial.flush( ); delay(258);
} // test33(
/// end

以下程序码多增加一个 test000( ) 函数里面的 for Loop 内做一个 NOP,
这是为了估计出 for LOOP 与 if 所用掉的额外时间 !

//共测试四种运算, 各做 200次(当然 for LOOP 本身也会浪费时间!)
//  test000( ) 每次都是做 NOP 一个 clock
//  test( ) 测试做 int 加法
//  test22( ) 做 String gg += 一个 char
//  test33( ) 也是做 String yy += 一个 char, 但有事先 yy.reserve(228);
//事实上如果只是做整数 int 加法,
//在 for Loop 和 if 用掉的时间远大于 ggyy 做 int 加法!

String gg = "";
String yy = "";
int ggyy = 0;
unsigned long begt, endt, runt;
void setup( ) {
  Serial.begin(9600);
  yy.reserve(228);
  test000( );
  test( );
  test22( );
  test33( );
}
void loop( ) {
  ;
}
void test( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) ggyy += '\n';
    else ggyy += 'A';
  }
  endt = micros( );
  Serial.println(String("ggyy is int, Run time = ") + (endt - begt) + "us");
  Serial.println(String("ggyy now = ") + ggyy + "\r\n");
  Serial.flush( ); delay(258);
} // test(
void test22( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) gg += '\n';
    else gg += 'A';
  }
  endt = micros( );
  Serial.println(String("gg No reserve, Run time = ") + (endt - begt) + "us");
  Serial.println(gg + "\r\n");
  Serial.flush( ); delay(258);
}
void test33( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) yy += '\n';
    else yy += 'B';
  }
  endt = micros( );
  Serial.println(String("yy.reserve(228), Run time = ") + (endt - begt) + "us");
  Serial.println(yy);
  Serial.flush( ); delay(258);
} // test33(
void test000( ) {
  int i = 0; delay(258);
  begt = micros( );
  for (i = 1; i <= 200; ++i) {
    if (i % 70 == 0) { //ggyy += '\n';
      __asm__("nop\n\t"); // 0.0000000625 秒==0.0625us
    } else { //
      __asm__("nop\n\t"); // 0.0000000625 秒==0.0625us
      //要做一点事避免被 compiler 拿掉
    }
  }
  endt = micros( );
  Serial.println(String("test000, Run time = ") + (endt - begt) + "us");
  Serial.println( );
  Serial.flush( ); delay(258);
} // test000(
/// end

以下是我的测试结果:
test000( ) 3068us
test( ) 3080us
test22( ) 8076us
test33( ) 5064us

因为 test000( ) 是做了 200个 NOP, 共 200 * 0.0625us = 12.5 us
就是说实际只做 12.5 us, 其他是因 for LOOP 与 if 引起的 overhead 额外时间,
该些额外时间共 3068 -12.5 = 大约 3055.5 us
所以, 实际上的时间是:
test( ) 做 200次 int 加法 用 3080-3055 = 25us
test22( ) 做 200次串接一个 char 用 8076-3055 = 5021us
test33( ) 因为有 .reserve( ); 做200次串接一个 char 用 5064-3055 = 2009us
所以, 做一次 字符串 += 一个 char; 如果有事先 reserve 内存, 每次平均约 10us,
如果没有事先 reserve 内存, 则每次平均约 15us,
另外, 由 test( ) 可估计出做一次 int += byte 平均约 25/200 = 0.125us,
其实就是用掉 2 clock cycle (tick), 这是因为通常编译程序会把变量(变量)安排在寄存器,
而一个 2 byte 的整数 (Arduino 的 int 是用 2 byte)做运算只要 2 clock cycles.

事实上如果不是先 reserve, 最大的问题不是比较慢而已,
而是很可能后来都要不到位置, 字符串根本没加进去, 但是 Arduino 却没报错也没让我们知道 !!

你自己可以串接更多更长字符串看看结果如何 !?

猜你喜欢

转载自blog.csdn.net/acktomas/article/details/88365219