在C语言中连接字符串,哪种方法更有效?

59

我遇到了这两种字符串连接方法:

共同部分:

char* first= "First";
char* second = "Second";
char* both = malloc(strlen(first) + strlen(second) + 2);

方法一:

strcpy(both, first);
strcat(both, " ");       // or space could have been part of one of the strings
strcat(both, second);

方法二:

sprintf(both, "%s %s", first, second);

两种情况下,both的内容都将是"First Second"

我想知道哪个更有效率(我必须执行多个连接操作),或者如果您知道更好的方法,请告诉我。


6
正如Michalis Giannakidis所指出的那样——这里存在缓冲区溢出;你需要分配长度加上“两个”来留出空间和终止符的位置。 - Jonathan Leffler
2
从性能的角度来看,要知道的是strcat必须扫描整个字符串以查找结尾,然后才能添加任何内容,而sprintf必须解析格式字符串。除此之外,如果你想知道哪种方法对于你特定的字符串更快,你必须进行测量。 - Steve Jessop
1
我想你也可以考虑sprintf函数比简单的字符串操作函数要大得多,因此很可能会从icache中驱逐更多的代码,因此更有可能减慢程序中与其完全无关的其他部分。但这种影响已经超出了你可以预测性能的范围。 - Steve Jessop
感谢您在这里提供的缓冲区溢出信息,我现在会进行编辑。也感谢您的评论,非常感激。 - Xandy
如果你需要大量字符串拼接,使用显式长度的字符串可能比使用以 null 结尾的字符串更值得。(std::string 知道自己的长度,但对于编译时常量字符串字面值可能无法进行优化) - Peter Cordes
10个回答

74

为了易读性,我会选择

char * s = malloc(snprintf(NULL, 0, "%s %s", first, second) + 1);
sprintf(s, "%s %s", first, second);

如果你的平台支持GNU扩展,你也可以使用asprintf()

char * s = NULL;
asprintf(&s, "%s %s", first, second);

如果你被MS C Runtime卡住了,你需要使用_scprintf()来确定最终字符串的长度:

char * s = malloc(_scprintf("%s %s", first, second) + 1);
sprintf(s, "%s %s", first, second);

以下很可能是最快的解决方案:

size_t len1 = strlen(first);
size_t len2 = strlen(second);

char * s = malloc(len1 + len2 + 2);
memcpy(s, first, len1);
s[len1] = ' ';
memcpy(s + len1 + 1, second, len2 + 1); // includes terminating null

18
我想要反对你提出的第一种解决方案可读性更高的说法。虽然它更加简洁,但是它真的更易读吗?我并不这样认为。不过我没有给它点踩。 - Imagist
2
也许值得一提的是 asprintf() 函数可以为你完成内存分配:char *s; int len = asprintf(&s, "%s %s", first, second); 调用简单方便,无需繁琐操作。 - Jonathan Leffler
1
@Jonathan:asprintf()不是C标准库的一部分,而且MS编译器不支持它。 - Christoph
1
@Christoph:是的,我知道asprintf()不是标准函数;这就是为什么我建议提到它而不是将其作为“答案”的原因。也许我应该在我的原始评论中加入相关警告。 (手册页面位于:http://linux.die.net/man/3/asprintf,以及其他地方。) - Jonathan Leffler
1
对于较短的字符串,内存分配将是主要瓶颈。此外,讨论不同的XXprintf函数是无关紧要的,因为这种方法显然是最慢的。 - noop
显示剩余3条评论

23

不必担心效率:让你的代码易读易维护。我怀疑这些方法之间的差异在你的程序中并不重要。


6
我和 Ned 在一起。你似乎在进行过早的优化。就像女孩子一样,这也是万恶之源(有多个根源)。先让程序跑起来,然后对其进行分析,再进行优化。在此之前,我认为你只是在浪费时间。 - freespace
19
@Ned:这并没有回答问题!他问的是哪种方法更有效,而不是他是否需要担心效率问题。 - Wadih M.
2
使用这种编程语言实际上意味着您确实关心效率。如果您不关心效率,为什么要使用具有手动内存管理的不安全、功能受限的语言呢?此外,性能分析被高估了。要么您理解自己的目标并可以预测可能的性能瓶颈,要么您一无所知,即使有性能分析工具的帮助也是如此。 - noop
2
我同意这可能是过早优化的情况,但重要的是要认识到(正如OP所做的那样)它最终可能会成为优化的情况。如果它最终成为瓶颈,并且这样的字符串连接在整个程序中都被执行,那么这将是一个问题。为了减轻这种风险和当然为了更好的可读性,我会将其分解为一个函数,比如strConstructConcat(),并将Method 1或Method 2放入其中,直到分析显示它成为瓶颈。 - Arun
4
-1并没有回答问题;另外,从问题中无法确定优化是否过早。给@Arun点赞,因为他实际上提议将其分解为函数以获得更多的灵活性(这确实可以帮助提问者)。 - griffin
显示剩余6条评论

19

我测量了一下,以下是一些有趣的结果:我使用了双核 P4,运行 Windows 操作系统,使用 mingw gcc 4.4 编译器,用以下命令编译 "gcc foo.c -o foo.exe -std=c99 -Wall -O2"。

我测试了原帖中的方法1和方法2。最初将malloc放在基准测试循环之外。结果发现方法1比方法2快了48倍。奇怪的是,从构建命令中删除-O2使得生成的exe文件快了30%(还没有调查原因)。

然后,我在循环内添加了一个malloc和free。这导致方法1的速度降低了4.4倍。而方法2的速度只降低了1.1倍。

因此,malloc + strlen + free并没有对性能产生足够大的影响,以至于避免使用sprintf不值得。

下面是我使用的代码(循环部分实现时使用了“<”而不是“!=”,但这会破坏本帖子的HTML呈现):

void a(char *first, char *second, char *both)
{
    for (int i = 0; i != 1000000 * 48; i++)
    {
        strcpy(both, first);
        strcat(both, " ");
        strcat(both, second);
    }
}

void b(char *first, char *second, char *both)
{
    for (int i = 0; i != 1000000 * 1; i++)
        sprintf(both, "%s %s", first, second);
}

int main(void)
{
    char* first= "First";
    char* second = "Second";
    char* both = (char*) malloc((strlen(first) + strlen(second) + 2) * sizeof(char));

    // Takes 3.7 sec with optimisations, 2.7 sec WITHOUT optimisations!
    a(first, second, both);

    // Takes 3.7 sec with or without optimisations
    //b(first, second, both);

    return 0;
}

1
刚刚查看了生成的指令,-O2 代码不仅更大而且更慢!问题似乎是gcc使用“repne scasb”指令来查找字符串的长度。我怀疑这个指令在现代硬件上非常慢。我将寻找gcc专家来咨询此事。 - Andrew Bainbridge
1
@Andrew Bainbridge,有点离题,但您可以使用<和>代替<和>。 - quinmars
1
@Andrew Bainbridge:您也可以通过缩进4个空格来格式化代码。这样,您就不必转义<和>,并且还可以获得语法高亮。 - bk1e
3
尝试使用“-march = generic”。mingw 的默认设置为 i586,这是非常古老、过时的,并做出了一些假设,不适合现代应用。请使用该选项来改善性能。 - LiraNuna
GCC不再内联repne scasb用于strlen。你是正确的,这对性能来说是一个糟糕的选择。调用一个可以使用SSE2或AVX2的库函数会更好,特别是对于中长字符串。它有时仍然会内联repe cmpsb用于与字符串字面量进行比较,当它们很短而不是函数调用的开销时,这可能是可以接受的。 - Peter Cordes
显示剩余2条评论

6
size_t lf = strlen(first);
size_t ls = strlen(second);

char *both = (char*) malloc((lf + ls + 2) * sizeof(char));

strcpy(both, first);

both[lf] = ' ';
strcpy(&both[lf+1], second);

1
那个strcat应该改为第二个strcpy - 按照现在的写法,这是未定义行为。 - Steve Jessop
2
实际上,可以使用memcpy,因为长度已经计算好了 :) - Filip Navara
但是,正如@onebyone指出的那样,这次不能使用strcat(),因为strcat()会在空格后开始跟踪,并且此时您不知道字符串中有哪些字符。 - Jonathan Leffler
3
@onebyone说过的话是:优化版本的memcpy()会在每次迭代步骤中复制多个字节; strcpy()也可能这样做,但它仍然需要检查每个单独的字节以检查终止0; 因此,我会预期memcpy()更快。 - Christoph
当然,我也希望对于长字符串,memcpy更快。但我觉得猜测可能导致直觉上不合理的事情发生是很有趣的。例如,在某些体系结构(包括MMX,我想)中,有打包比较操作,因此strcpy可以在一条指令中检查复制块中的所有字节... - Steve Jessop
显示剩余3条评论

2

它们应该基本相同。区别并不重要。我会选择 sprintf,因为它需要更少的代码。


2
差异不太重要:
  • 如果您的字符串很小,则malloc会淹没字符串连接。
  • 如果您的字符串很大,则复制数据所花费的时间将淹没strcat / sprintf之间的差异。
正如其他帖子中提到的,这是一种过早的优化。集中精力于算法设计,只有在分析显示它是性能问题时才回来解决。
话虽如此...我猜测方法1会更快。解析sprintf格式字符串存在一些-尽管很小的-开销。而strcat更可能是“内联的”。

strcat版本会扫描first字符串的全部长度四次,而sprintf版本仅扫描两次。因此,当first字符串非常长时,strcat版本最终会变得更慢。 - caf

1

sprintf()被设计用来处理远不止字符串的内容,strcat()是专门为此而设。但我怀疑你正在为小事烦恼。C语言字符串在基本上是低效的,这使得这两种方法之间的差异微不足道。请读Joel Spolsky的《回到基础》以了解更多细节。

这是一个C++通常比C表现更好的实例。对于重量级字符串处理,使用std::string可能更有效,并且肯定更安全。

[编辑]

[第二次编辑]修正了代码(C字符串实现中迭代次数过多),时间和结论相应改变

我对Andrew Bainbridge的评论感到惊讶,他没有发布完整的测试代码。我修改了他的代码(自动计时)并添加了一个std::string测试。测试是在VC++ 2008(本机代码)默认的“Release”选项下(即优化)进行的,Athlon双核2.6GHz。结果:

C string handling = 0.023000 seconds
sprintf           = 0.313000 seconds
std::string       = 0.500000 seconds

所以在这里,strcat()明显更快(根据编译器和选项的不同,结果可能会有所不同),尽管C字符串约定本身存在效率低下的问题,并且支持我的原始建议:sprintf()携带了很多不必要的负担。然而,它仍然是最不易读和安全的,因此当性能不是关键因素时,我认为它没有太大的价值。

我还测试了std::stringstream实现,但速度再次慢得多,但对于复杂的字符串格式化仍然有一定的优点。

以下是已更正的代码:

#include <ctime>
#include <cstdio>
#include <cstring>
#include <string>

void a(char *first, char *second, char *both)
{
    for (int i = 0; i != 1000000; i++)
    {
        strcpy(both, first);
        strcat(both, " ");
        strcat(both, second);
    }
}

void b(char *first, char *second, char *both)
{
    for (int i = 0; i != 1000000; i++)
        sprintf(both, "%s %s", first, second);
}

void c(char *first, char *second, char *both)
{
    std::string first_s(first) ;
    std::string second_s(second) ;
    std::string both_s(second) ;

    for (int i = 0; i != 1000000; i++)
        both_s = first_s + " " + second_s ;
}

int main(void)
{
    char* first= "First";
    char* second = "Second";
    char* both = (char*) malloc((strlen(first) + strlen(second) + 2) * sizeof(char));
    clock_t start ;

    start = clock() ;
    a(first, second, both);
    printf( "C string handling = %f seconds\n", (float)(clock() - start)/CLOCKS_PER_SEC) ;

    start = clock() ;
    b(first, second, both);
    printf( "sprintf           = %f seconds\n", (float)(clock() - start)/CLOCKS_PER_SEC) ;

    start = clock() ;
    c(first, second, both);
    printf( "std::string       = %f seconds\n", (float)(clock() - start)/CLOCKS_PER_SEC) ;

    return 0;
}

@Xandy:这个基准测试非常虚假,因为gcc将a()内联到main()中,从而可以看到编译时常量字符串并将strcpy/strcat内联。例如,gcc4.4(https://godbolt.org/g/Fsvh9D)使用`mov DWORD [rbx], 0x73726946(4个ASCII字符作为立即数据存储指令),然后是另一个2字节的存储。它确实调用了实际的strlen`库函数两次,但所有操作都只是非常便宜的mov-immediate和指针加法。 - Peter Cordes
@PeterCordes:八年过去了,我不确定这是否重要,但是声明“first”和“second”为“volatile”应该可以解决您的问题。我没有更改答案,因为那可能会改变结果,而且我无法在当时使用的平台和编译器上重新运行它。如果我再次看到这个问题,我可能会发布未经优化的结果,因为对于一般比较来说,这可能更好,因为“特殊情况”可能不适用于一般情况,不会被优化掉。 - Clifford
@Clifford:是的,volatile可能有帮助,但我不确定它是否是最好的选择。设计微基准测试很困难;通常你必须查看汇编代码,以确定它是否适用于实际用例。另一个选项是使用全局char*变量,这样main就不能假设它们具有原始值,除非你使用整个程序优化。(任何静态初始化器都可能修改它们)。但是main仍然可以将2个输入的strlen提升到基准循环之外。还有一件有用的事情是__attribute__((noinline)) - Peter Cordes
@PeterCordes:我认为在“复杂度比较”而不是“它是否足够快”的测试中,未经优化的代码仍然是公平的——问题是“哪个更快”,而不是“有多快”。优化器可以发现可能不适用于一般情况并扭曲比较的特殊情况。我的评论2009年9月6日11:31适用于具有特定性能要求的特定代码。此外,C++对我来说可能很有趣,但这是一个严格的C问题,回想起来我可能不应该包括它在内,因为我有更多的SO经验。 - Clifford
你的评论仍然表明未经优化是找出“哪个更快”的合理方法。这在一般情况下并不正确,即使在一个sprintf与多个更简单的库函数之间的比较中,我也会谨慎对待。制作微基准测试的正确方法是阻止编译器发现任何您不想要的优化,而不是在所有地方禁用优化。 - Peter Cordes
显示剩余15条评论

0

我不知道在第二种情况下是否真的进行了任何连接。将它们背靠背地打印并不构成连接。

告诉我,哪个更快:

1)a)将字符串A复制到新缓冲区 b)将字符串B复制到缓冲区 c)将缓冲区复制到输出缓冲区

还是

1)将字符串A复制到输出缓冲区 b)将字符串B复制到输出缓冲区


OP 提议使用 sprintf(dst, "%s %s", first, second) 在内存中连接字符串,而不是使用普通的 printf。对于你所建议的内容,可能一次调用 printf 是最快的方式,因为 fputs / putchar / fputs 需要多次锁定 / 解锁 stdout(或任何输出流)。 - Peter Cordes

0
  • 相比于需要解析格式字符串的sprintf,strcpy和strcat操作要简单得多。
  • 由于strcpy和strcat很小,因此编译器通常会将它们内联,从而节省更多的函数调用开销。例如,在llvm中,strcat将使用strlen来查找复制起始位置,然后是一个简单的存储指令进行内联。

-1

两种方法都不是非常高效,因为它们每次都需要计算字符串长度或扫描字符串。相反,既然您已经计算了单个字符串的strlen(),那么将它们放入变量中,然后只需使用strncpy()两次即可。


5
如果他知道字符串的大小,为什么要使用strncpy()?使用memcpy()更好! - Christoph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接