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+= 任何数;
之时, 它会做:
- 先检查 += 右边的, 如果是 int 就先转换成字符串; 如果是 float 或 double, 要建立
临时字符串 = 整数部分 + "." + 小数点后取两位
(从小数点后第三位四舍五入) - 算出在(1)的临时字符串的长度,注意, 这时的临时字符串还是 C 的字符串, 不是 C++ 的 String,算长度只能一个 char 一个 char 算, 直到遇见代表字符串结束的 0 才知道!
- 取出 gg 的长度(C++ 的 String 有记住字符串长度),其实 C++ 的 String 至少记住三样数据 :
- 指针(pointer;指标)指向一个 char Array, 此 Array 是用 C 语言存放字符串方式储存字符串!
- 该 char Array 的空间大小 (Capacity)
- 已经真正用掉几个 char, 这就是字符串的长度 length
- 看看如果把右边字符串加上去之后原先 gg 的空间 capacity 够不够?如果位置够用, 到(5); 如果位置不够用, 做:
- 先把 gg 的位置(就是在(3)(a)说的指针)用临时变量记住
- 算出 len = gg 原先字符串长度 + 右边临时字符串长度
- 去要(malloc)一块连续 len 个 char 的内存, 把 gg 的指针指向该新要来的位置
- 把旧的字符串全部复制到新的位置(调用 C 语言的 strcpy()函数)
- 把在(a)临时记住原先 gg 的指针指过去的内存全部还给系统
- 更新 gg 的 capacity 与 length
- 把右边要加的字符串加入到 gg 字符串的尾巴! 这动作是依据 gg 的指针指过去的地方加上原先的 length, 调用 strcpy( ) 把右边临时字符串复制到该处开始的地方, 更新 gg 的 length
- 请注意, 这时 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 却没报错也没让我们知道 !!
你自己可以串接更多更长字符串看看结果如何 !?