2014年3月20日星期四

为什么不用动态内存分配?

在写这篇Blog的时候,我考虑了几分钟,在想要不要把标题写成《为什么有的程序员喜欢用动态内存分配?》。最后我还是把那些修饰词和定语给删了。虽然那个标题更准确一点,但是本文基本上是一篇吐槽文,我还是比较喜欢这种反问句的感觉。

事情是这样开始的:
在工作中,遇到了别的同事以前写的一段代码。作用是显示从某些网上下载的文件的内容。文件下载完后,也在本地保存了一份副本,这样如果下次发现本地有副本,就直接显示不用下载了。
这基本上是一个类似浏览器缓存的功能,实现起来也不难。不过这次我碰到一个Bug,有个文件的副本,在解析的时候报错了。
因为第一次下载的时候并没有报错,所以焦点就集中到这个缓存机制上。这里面有个值得关注的地方在于,大概是出于节省本地硬盘空间的考虑,本地的副本在保存时是压缩过的。于是问题可能出在两个地方:

  • 压缩算法有问题,压缩保存的时候,把文件给弄坏了。
  • 解压缩算法有问题,无法正确还原这个文件。

这套压缩/解压缩的算法,是开源的(zlib)。所以我认为问题不应该出在算法本身,更可能是用法没用对。调用代码大概是这个样子的:
#define chunk 16384
void compress_file(const char* source_file , const char* dest_file)
{
    unsigned char datein[chunk];
    unsigned char dateout[chunk];
    unsigned long datelong = chunk;
    unsigned long sourcelong;
    FILE* source;
    FILE* dest;
    source = fopen(source_file , "r");
    dest = fopen(dest_file, "w+b");
    while (!feof(source))
    {
        sourcelong = fread(datein, 1, chunk, source);
        compress(dateout, &datelong, datein, sourcelong, 1);
        fwrite(dateout, datelong, 1, dest);
    }
    fclose(source);
    fclose(dest);
}
void un_compress_file(const char* source_file , const char* dest_file)
{
    unsigned char datein[chunk];
    unsigned char dateout[chunk];
    unsigned long datelong = chunk;
    unsigned long sourcelong;
    FILE* source;
    FILE* dest;
    source = fopen(source_file , "r+b");
    dest = fopen(dest_file , "w");
    while (!feof(source))
    {
        sourcelong = fread(datein, 1, chun, source);
        datelong = chunk;
        if (uncompress(dateout, &datelong, datein, sourcelong))
        {
            fwrite(dateout, datelong, 1, dest );
        }
    }
    fclose(source);
    fclose(dest);
}
这段代码我也不打算在这里分析太多,问题很明显:代码编写的初衷,是想把文件分块处理。但每块数据压缩之后的大小并没有记录在压缩文件中,也没有采取一些诸如分隔符或区块补齐之类的定位措施,所以解压缩的时候实际上是无法忠实地按压缩时的分块来还原数据的。而出问题的那个文件,大小的确是超过了16384,于是就被弄坏了。

这里就引出了一个问题:为什么要分块?
事实上,如果这段代码没有采用固定长度的C-style数组,而是用动态内存分配的解决方案,压根都不会需要分块,也就不会出现这个Bug。当然,这只是解决这个Bug的方案之一。对分块压缩算法的理解有问题,也是造成这个Bug的原因之一。从这方面着手进行改进也是可以的,各有利弊而已。
但这不是我要表达的重点。在这个案例里,下载的文件并不会很大,几十KB就顶天了。我真正疑惑的地方在于:为什么不用动态内存分配?
可能的解释有:
  • 担心内存碎片问题
  • 担心忘记释放
  • 嫌动态分配内存麻烦
  • 习惯了这种固定长度缓冲区的写法
  • ……
也许还有别的原因,一时半会儿我是想不到了。

那么换个问题:什么时候该用动态内存分配?
这个答案会比较明确一点:
  • 空间大小不确定(运行期确定)
  • 栈上空间不够
  • 方便与线程外部传递/分享数据

在本文的这个例子中,文件的长度是不确定的,每块数据压缩后的长度也是不确定的。很明显,这就是属于应该用上动态内存分配的时候。
该用的时候不用,带来的恶果就是程序的可读性和可维护性就会变得差,出Bug的机会更高。毕竟固定长度的内存区域就一定要处理溢出问题。而且用固定长度去处理变长内容,要分块/分次,要做循环,要留意退出条件,测试时要覆盖1和N……,这些都带来了不必要的开销。
还不如直接分配一块内存出来,只要到时候记得回收就OK。性能方面值得担心的话,也可以自己优化内存管理,这是可以集中处理掉的事情。而那种用固定长度的栈缓冲区来解决此类问题的办法,好听一点叫做“质朴”,难听一点叫“土”。总不能每个需要动态内存分配的地方,都用这种土办法来应对吧。

我其实是觉得,有些程序员,会有意识(或下意识)地避免用动态内存分配。从写代码的时候就开始重视性能,是好事情,但写程序不能只看功能和性能。你写的程序,好不好懂,容不容易出问题,有没有定时炸弹,好不好改,方不方便扩展,这些也都是很重要的。性能不佳可以优化,这种代码级的性能问题(相比架构级而言)优化起来尤其容易。但其它的方面,要改善起来绝非一日之功。
往开了说,作为程序员,应该避免陷入“某个东西就是不好”的思维方式中。思维开始变得狭隘,是自身没法继续再提高(达到上限了)的标志之一。