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


https://www.arduino.cn/thread-12558-1-1.html

问题:为何要用reserve()

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: 这样写到底有啥小路用呢 ?
    这会让你的程序做 gg += (char)yy; 的时候会快一点 !

  • Q: 至于像 gg += (char)yy; 是在何时会这样写呢?
    通常你是当 Serial.available( ) 之时做类似:

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

或是会这样写:

    gg = "";
    while( Serial.available( ) ) {
      gg += (char)yy;
      delay(2);
    } 
  • Q: 那为何写了 gg.reserve(200); 就会使得 gg += (char)yy; 比较快呢?
    这是因为 String 类别运作方式的关系! 因为 String 其实是 C++ 的类, 它不是 C 的字符串, 很好用, 但相对比 C 的字符串 ·char haha[ ] = "hello you!"; 慢很多!

String类的运行机制

在 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 至少记住三样数据 :
    • 指针(pointer;指标)指向一个 char Array, 此 Array 是用 C 语言存放字符串方式储存字符串!
    • 该 char Array 的空间大小 (Capacity)
    • 已经真正用掉几个 char, 这就是字符串的长度 length
  4. 看看如果把右边字符串加上去之后原先 gg 的空间 capacity 够不够?如果位置够用, 到(5); 如果位置不够用, 做:
    • 先把 gg 的位置(就是在(3)(a)说的指针)用临时变量记住
    • 算出 len = gg 原先字符串长度 + 右边临时字符串长度
    • 去要(malloc)一块连续 len 个 char 的内存, 把 gg 的指针指向该新要来的位置
    • 把旧的字符串全部复制到新的位置(调用 C 语言的 strcpy()函数)
    • 把在(a)临时记住原先 gg 的指针指过去的内存全部还给系统
    • 更新 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 这时只是不管你, 不会发处错误信息 !

Reserve()优势

如果你在 setup( ) 内事先做了 gg.reserve(200); 则它会先去要一块内存, 可以放 200+1 = 201 char 的位置(别忘了最后要放整数 0 表示字符串结束!), 那么以后, 只要你的字符串 gg 的总长度没超过 200个char, 就永远用旧的位置, 不会做要新位置又复制等繁杂的工作 !
请注意, 写 gg = "abcdef";这样也是可能需要去跟系统要新位置并把旧的位置占用的内存还掉 !就是原先的 capacity 容量不够放得下等号右边的字符串之时就必须做这些复杂的动作 !!

关于 String class 的源代码可参考:

https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/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

测试2:减去for,if所用时间

以下程序码多增加一个 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 却没报错也没让我们知道 !!

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值