为什么这个程序在Python中比在Objective-C中更快?

18 投票
5 回答
1353 浏览
提问于 2025-04-16 15:28

我对这个小例子产生了兴趣,内容是用Python编写的算法,用来遍历一个很大的单词列表。我正在写一些“工具”,希望能像Python那样对Objective-C中的字符串或数组进行切片操作。

特别是,这个优雅的解决方案引起了我的注意,因为它执行得非常快,并且在算法中使用了字符串切片作为关键元素。试着不使用切片来解决这个问题!

我在下面用Moby单词列表复现了我的本地版本。如果你不想下载Moby,可以使用/usr/share/dict/words。这个源文件就是一个包含独特单词的大字典列表。

#!/usr/bin/env python

count=0
words = set(line.strip() for line in   
           open("/Users/andrew/Downloads/Moby/mwords/354984si.ngl"))
for w in words:
    even, odd = w[::2], w[1::2]
    if even in words and odd in words:
        count+=1

print count      

这个脚本将会 a) 被Python解释;b) 读取4.1 MB、354,983个单词的Moby字典文件;c) 去掉行首行尾的空白;d) 将这些行放入一个集合中;e) 找出所有组合,其中给定单词的偶数和奇数部分也是单词。在MacBook Pro上,这个过程大约需要0.73秒。

我尝试用Objective-C重写同样的程序。我对这个语言还是个初学者,所以请多包涵,但也请指出我的错误。

#import <Foundation/Foundation.h>

NSString *sliceString(NSString *inString, NSUInteger start, NSUInteger stop, 
        NSUInteger step){
    NSUInteger strLength = [inString length];

    if(stop > strLength) {
        stop = strLength;
    }

    if(start > strLength) {
        start = strLength;
    }

    NSUInteger capacity = (stop-start)/step;
    NSMutableString *rtr=[NSMutableString stringWithCapacity:capacity];    

    for(NSUInteger i=start; i < stop; i+=step){
        [rtr appendFormat:@"%c",[inString characterAtIndex:i]];
    }
    return rtr;
}

NSSet * getDictWords(NSString *path){

    NSError *error = nil;
    NSString *words = [[NSString alloc] initWithContentsOfFile:path
                         encoding:NSUTF8StringEncoding error:&error];
    NSCharacterSet *sep=[NSCharacterSet newlineCharacterSet];
    NSPredicate *noEmptyStrings = 
                     [NSPredicate predicateWithFormat:@"SELF != ''"];

    if (words == nil) {
        // deal with error ...
    }
    // ...

    NSArray *temp=[words componentsSeparatedByCharactersInSet:sep];
    NSArray *lines = 
        [temp filteredArrayUsingPredicate:noEmptyStrings];

    NSSet *rtr=[NSSet setWithArray:lines];

    NSLog(@"lines: %lul, word set: %lul",[lines count],[rtr count]);
    [words release];

    return rtr;    
}

int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    int count=0;

    NSSet *dict = 
       getDictWords(@"/Users/andrew/Downloads/Moby/mwords/354984si.ngl");

    NSLog(@"Start");

    for(NSString *element in dict){
        NSString *odd_char=sliceString(element, 1,[element length], 2);
        NSString *even_char=sliceString(element, 0, [element length], 2);
        if([dict member:even_char] && [dict member:odd_char]){
            count++;
        }

    }    
    NSLog(@"count=%i",count);

    [pool drain];
    return 0;
}

Objective-C版本的结果是一样的(13,341个单词),但花了将近3秒。我一定是做错了什么,导致一个编译语言的速度比脚本语言慢了超过3倍,但我实在看不出原因。

基本算法是一样的:读取行,去掉空白,然后放入集合中。

我猜慢的原因可能是处理NSString元素的过程,但我不知道有什么替代方法。

编辑

我把Python代码改成了这样:

#!/usr/bin/env python
import codecs
count=0
words = set(line.strip() for line in 
     codecs.open("/Users/andrew/Downloads/Moby/mwords/354984si.ngl",
          encoding='utf-8'))
for w in words:
    if w[::2] in words and w[1::2] in words:
        count+=1

print count 

为了让utf-8与utf-8的NSString在同一层面上。这让Python的执行时间变成了1.9秒。

我还把切片测试改成了短路类型,正如这个建议所说的,Python和Objective-C版本的速度现在差不多了。我还尝试使用C数组而不是NSString,这样速度快了很多,但不太容易。而且这样会失去对utf-8的支持。

Python真的很酷……

编辑2

我发现了一个瓶颈,显著加快了速度。与其使用[rtr appendFormat:@"%c",[inString characterAtIndex:i]];方法将字符添加到返回字符串中,我用了这个:

for(NSUInteger i=start; i < stop; i+=step){
    buf[0]=[inString characterAtIndex:i];
    [rtr appendString:[NSString stringWithCharacters:buf length:1]];
}

现在我终于可以说Objective-C版本的速度比Python版本快了——但差别不大。

5 个回答

2

有个链接提到,你可以用CFSet来替代NSSet,这样可能会让程序运行得更快。

我在网上快速搜索了一下,没找到NSSet和CFSet的具体实现方式:如果它们是基于树的实现(就像C++中的set那样),那么查找和检查的时间复杂度是O(log(N)),而Python的集合查找时间复杂度是O(1),这可能就是速度差异的原因。

[编辑] ncoghlan在下面的评论中提到,Objective-C中的集合使用的是哈希表,所以查找时间也是常量级的。因此,这就取决于Python和Objective-C中集合的实现效率了。正如其他人所指出的,Python的集合和字典经过了大量优化,尤其是在字符串作为键的时候(因为Python的字典用于实现命名空间,需要非常快)。

2

在这两段代码中,你先创建了偶数和奇数的部分,然后再用这些部分去测试单词。如果你能先确保偶数部分成功,再去创建奇数部分,那会更好。

现在的Python代码:

even, odd = w[::2], w[1::2]
if even in words and odd in words:

更好的做法:

# even, odd = w[::2], w[1::2]
if w[::2] in words and w[1::2] in words:

顺便提一下,你没有提到的一个指标是:你写每段代码花了多长时间?

9

请记住,Python版本的代码在运行时会把很多复杂的工作转移到经过高度优化的C代码中,特别是在CPython环境下。这包括文件输入的缓冲、字符串切片和哈希表查找,以检查evenodd是否在words中。

不过,你似乎在你的Objective-C代码中是以UTF-8格式解码文件,而在Python代码中则是以二进制格式处理文件。使用Unicode的NSString在Objective-C中,而在Python中使用的是8位字节字符串,这样的比较并不公平。如果你在Python中使用codecs.open()"utf-8"的编码打开文件,我预计Python版本的性能会明显下降。

另外,在你的Objective-C代码中,你还进行了第二次完整的遍历来去除空行,而在Python代码中并没有这样的步骤。

撰写回答