C++编程风格这本书前面一些章节都觉得很简明易懂,但是读到效率这一章是才充分认识到读别人的代码还是很痛苦的一件事。书中给出的需要改进的初始类如下:
class BigInt { private: char* digits; unsigned ndigits; BigInt(char *d,unsigned n) { digits = d; ndigits = n; } friend class DigitStream; public: BigInt(const char*); BigInt(unsigned n = 0); BigInt(const BigInt&); void operator=(const BigInt&); BigInt operator+(const BigInt&) const; void print(FILE* f = stdout) const; ~BigInt() {delete [] digits;} }; class DigitStream { private: char* dp; unsigned nd; public: DigitStream(const BigInt& n) { dp = n.digits; nd = n.ndigits; } unsigned operator++() { if(nd == 0) return 0; else { nd--; return *dp++; } } }; void BigInt::print(FILE* f) const { for(int i = ndigits - 1;i >= 0;i --) fprintf(f,"%c",digits[i]+'0'); } void BigInt::operator=(const BigInt& n) { if (this == &n) return; delete [] digits; unsigned i = n.ndigits; digits = new char[ndigits = i]; char* p = digits; char* q = n.digits; while(i--) *p++ = *q++; } BigInt BigInt::operator+(const BigInt& n) const { unsigned maxDigits = (ndigits > n.ndigits ? ndigits : n.ndigits) + 1; char* sumPtr = new char[maxDigits]; BigInt sum(sumPtr,maxDigits); DigitStream a(*this); DigitStream b(n); unsigned i = maxDigits; unsigned carry = 0; while (i --) { *sumPtr = (++a) + (++b) + carry; if(*sumPtr >= 10) { carry = 1; *sumPtr -= 10; } else carry = 0; sumPtr++; } return sum; } BigInt::BigInt(unsigned n) { char d[3*sizeof(unsigned)+1]; char *dp = d; ndigits = 0; do { *dp++ = n % 10; n /= 10; ndigits++; } while(n > 0); digits = new char[ndigits]; for(register int i = 0;i < ndigits;i++) digits[i] = d[i]; } BigInt::BigInt(const BigInt& n) { unsigned i = n.ndigits; digits = new char[ndigits = i]; char* p = digits; char* q = n.digits; while(i--) *p++ = *q++; } BigInt::BigInt(const char* digitString) { unsigned n = strlen(digitString); if(n != 0) { digits = new char[ndigits=n]; char* p = digits; const char* q = &digitString[n]; while(n--) *p++ = *--q - '0'; } else { digits = new char[ndigits=1]; digits[0] = 0; } }
因为这一章讨论的是效率问题,所以我们将统计代码的运行时间,查看代码是否存在内存泄露的代码都加入程序中。因为用的机器比书上的机器快,所以把循环增加到了10000次,耗时4.7秒。
void test(){ clock_t start = clock(); BigInt b = 1; for(int i=1; i <= 10000; ++i){ b = b + 1; } b.print(); clock_t end = clock(); cout << endl << static_cast<double>(end - start)/CLOCKS_PER_SEC << endl; } int _tmain(int argc, _TCHAR* argv[]) { test(); _CrtDumpMemoryLeaks(); cin.get(); return 0; }
觉得原始代码晦涩的同志们肯定一眼就能发现DigitStream真是很多余的东西,影响我们对代码的理解,将DigitStream去掉,得到代码
class BigInt { private: char* digits; unsigned ndigits; BigInt(char *d,unsigned n) { digits = d; ndigits = n; } char fetch(int i) const { return i < ndigits ? digits[i] : 0; } public: BigInt(const char*); BigInt(unsigned n = 0); BigInt(const BigInt&); void operator=(const BigInt&); BigInt operator+(const BigInt&) const; void print(FILE* f = stdout) const; ~BigInt() {delete [] digits;} }; void BigInt::print(FILE* f) const { for(int i = ndigits - 1;i >= 0;i --) fprintf(f,"%c",digits[i]+'0'); } void BigInt::operator=(const BigInt& n) { if (this == &n) return; delete [] digits; unsigned i = n.ndigits; digits = new char[ndigits = i]; char* p = digits; char* q = n.digits; while(i--) *p++ = *q++; } BigInt BigInt::operator+(const BigInt& n) const { unsigned maxDigits = (ndigits > n.ndigits ? ndigits : n.ndigits) + 1; char* sumPtr = new char[maxDigits]; BigInt sum(sumPtr,maxDigits); unsigned carry = 0; for(int i=0; i<maxDigits; ++i) { *sumPtr = fetch(i) + n.fetch(i) + carry; if(*sumPtr >= 10) { carry = 1; *sumPtr -= 10; } else carry = 0; sumPtr++; } return sum; } BigInt::BigInt(unsigned n) { char d[3*sizeof(unsigned)+1]; char *dp = d; ndigits = 0; do { *dp++ = n % 10; n /= 10; ndigits++; } while(n > 0); digits = new char[ndigits]; for(register int i = 0;i < ndigits;i++) digits[i] = d[i]; } BigInt::BigInt(const BigInt& n) { unsigned i = n.ndigits; digits = new char[ndigits = i]; char* p = digits; char* q = n.digits; while(i--) *p++ = *q++; } BigInt::BigInt(const char* digitString) { unsigned n = strlen(digitString); if(n != 0) { digits = new char[ndigits=n]; char* p = digits; const char* q = &digitString[n]; while(n--) *p++ = *--q - '0'; } else { digits = new char[ndigits=1]; digits[0] = 0; } }
运行,速度略有加快,耗时4.4秒,但无明显改进。
我们注意到之前的输出中前面都有那么多的0,这些0实际上都是木有必要的。其根源在于我们做加法运算的时候不管有没有进位,都默认进了一位,也就是每做一次加法运算,数位的长度都至少加一位,这在速度和空间上都是一种浪费。最简单的解决方法是每次加完以后都检测一下最高位,这样将加法改进如下:
BigInt BigInt::operator+(const BigInt& n) const { unsigned maxDigits = (ndigits > n.ndigits ? ndigits : n.ndigits) + 1; char* sumPtr = new char[maxDigits]; BigInt sum(sumPtr,maxDigits); unsigned carry = 0; for(int i=0; i<maxDigits; ++i) { *sumPtr = fetch(i) + n.fetch(i) + carry; if(*sumPtr >= 10) { carry = 1; *sumPtr -= 10; } else carry = 0; sumPtr++; } if(sum.digits[maxDigits-1]==0){ --sum.ndigits; } return sum; }
这一次的改进可以说是质的飞跃,耗时一下子缩短为0.069秒。输出的结果里,那些讨厌的0也消失了。
为了做进一步的改进,我们把循环改为一百万,运行耗时6.2秒
再次分析程序,发现在赋值时,如果位数相同,不需要重新分配空间,根据这个思路,将赋值函数修改为:
void BigInt::operator=(const BigInt& n) { if (this == &n) return; unsigned i = n.ndigits; if(ndigits != i){ delete [] digits; digits = new char[i]; } ndigits = i; char* p = digits; char* q = n.digits; while(i--) *p++ = *q++; }
运行,时间缩短为5.1秒
再次分析b = b + 1这个过程,先做加法运算,再做拷贝构造,赋值运算,其中加法运算需要分配空间,构造对象和逐个计算赋值,拷贝构造需要分配空间和逐个赋值,赋值运算只在需要的时候重新分配空间(次数可忽略),但是会逐个赋值。
一共分配空间两次,逐个赋值3次,逐个计算一次。
我们将这个加法过程进行优化,先构造对象再做+=运算和赋值,其中构造对象需要分配空间和逐个赋值,+=运算需要逐个计算(只在需要时分配空间,可忽略),assign在需要的时候分配空间(次数可忽略)和逐个赋值
一共分配空间一次,逐个赋值2次,逐个计算一次,优化后的代码如下:
class BigInt { private: char* digits; unsigned ndigits; unsigned size; BigInt (const BigInt&, const BigInt&); char fetch (unsigned i) const { return i < ndigits ? digits[i] : 0; } public: friend BigInt operator+(const BigInt&,const BigInt&); BigInt (const char*); BigInt (unsigned n = 0); BigInt (const BigInt&); BigInt& operator= (const BigInt&); BigInt& operator+= (const BigInt&); void print (FILE* f = stdout) const; ~BigInt() {delete [] digits;} }; inline BigInt operator+(const BigInt& left, const BigInt& right) { return BigInt(left,right); } BigInt& BigInt::operator+=(const BigInt& rhs) { unsigned max = 1+(rhs.ndigits > ndigits ? rhs.ndigits : ndigits); if(size < max) { char *d = new char[size = max]; for(unsigned i = 0;i < ndigits;++i) d[i] = digits[i]; delete [] digits; digits = d; } while(ndigits < max) digits[ndigits++] = 0; for(unsigned i = 0;i < ndigits;++i) { digits[i] += rhs.fetch(i); if(digits[i]>=10) { digits[i] -= 10; digits[i+1] += 1; } } if(digits[ndigits - 1] ==0) --ndigits; return *this; } void BigInt::print (FILE* f) const { for (int i = ndigits - 1; i >= 0; i --) fprintf (f, "%c", digits[i] + '0'); } BigInt& BigInt::operator= (const BigInt& rhs) { if (this == &rhs) return *this; ndigits = rhs.ndigits; if (ndigits > size) { delete [] digits; digits = new char[size = ndigits]; } for(unsigned i = 0;i < ndigits;++i) digits[i] = rhs.digits[i]; return *this; } BigInt::BigInt(const BigInt& left, const BigInt& right) { size = 1 + (left.ndigits > right.ndigits ? left.ndigits : right.ndigits); digits = new char[size]; ndigits = left.ndigits; for (unsigned i = 0;i < ndigits;++i) digits[i] = left.digits[i]; *this += right; } BigInt::BigInt (unsigned u) { char v = u; for (ndigits = 1;(v/=10) > 0;++ndigits) ; digits = new char[size = ndigits]; for(unsigned i = 0;i < ndigits;++ i) { digits[i] = u % 10; u /=10; } } BigInt::BigInt (const BigInt& copyFrom) { size = ndigits = copyFrom.ndigits; digits = new char[size]; for(unsigned i = 0;i < ndigits;++i) digits[i] = copyFrom.digits[i]; } BigInt::BigInt (const char* digitString) { if(digitString[0] == '/0') digitString = "0"; size = ndigits = strlen(digitString); digits = new char[size]; for(unsigned i = 0;i < ndigits;++i) digits[i] = digitString[ndigits - 1 - i] - '0'; }
运行,时间缩短为2.6秒
最后,优化客户代码,用b += 1;来代替b = b + 1;时间再次缩短为1.4秒