lua三种字符串拼接性能分析

说明

lua中提供了3中字符串拼接函数,测试在日常使用情况下,各种拼接字符串的性能对比。

测试环境

操作系统:Debian GNU/Linux 8
CPU:Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz
内存:64G
lua环境:LuaJIT-2.1.0-beta3 (测试的时候关闭jit)

测试

普通日常使用性能分析

日常拼接字符串的时候一般都是多个已存在的变量,夹杂一些字符串常量进行拼接,例如:

1
package.cpath = pg.script_dir .. "/bot/lib/?.so;" .. id .. package.cpath
  • 测试代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    -- 关闭jit
    if jit then
    jit.off()
    end

    -- 随机生成字符串备用
    local randomStringSzie = 200000
    local strTable = {}
    function makeStrTable()
    for i=1, randomStringSzie do
    strTable[#strTable + 1] = tostring(math.random(1, randomStringSzie*10))
    end
    end

    -- 辅助函数,用来打印时间
    function showtime(f)
    local start = os.clock()
    f()
    print(os.clock() - start)
    end

    local testCount = 50000

    -- string.format
    function test1_1_0()
    io.write("test1_1_0: use string.format: ")
    for i=1, testCount do
    local str = string.format("%s-string1-%s-string2-%s", strTable[math.random(1, randomStringSzie)],
    strTable[math.random(1, randomStringSzie)],
    strTable[math.random(1, randomStringSzie)])
    end
    end

    -- ..
    function test1_2_0()
    io.write("test1_2_0: use ..: ")
    for i=1, testCount do
    local str = strTable[math.random(1, randomStringSzie)] .. "-string1-" ..
    strTable[math.random(1, randomStringSzie)] .. "-string2-" ..
    strTable[math.random(1, randomStringSzie)]
    end
    end

    -- table.concat
    function test1_3_0()
    io.write("test1_3_0: use table.concat: ")
    for i=1, testCount do
    local str = table.concat({strTable[math.random(1, randomStringSzie)], "-string1-",
    strTable[math.random(1, randomStringSzie)], "-string2-",
    strTable[math.random(1, randomStringSzie)]})
    end
    end

    makeStrTable()

    collectgarbage("collect") -- 防止gc影响,先清除一遍
    showtime(test1_1_0)

    collectgarbage("collect") -- 防止gc影响,先清除一遍
    showtime(test1_2_0)

    collectgarbage("collect") -- 防止gc影响,先清除一遍
    showtime(test1_3_0)
  • 为了避免字符串拼接函数执行先后顺序的影响,三个函数轮流被注释,单独执行5次的测试结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    test1_1_0: use string.format: 0.029643
    test1_1_0: use string.format: 0.029626
    test1_1_0: use string.format: 0.029654
    test1_1_0: use string.format: 0.029586
    test1_1_0: use string.format: 0.029663

    test1_2_0: use ..: 0.025584
    test1_2_0: use ..: 0.025809
    test1_2_0: use ..: 0.025492
    test1_2_0: use ..: 0.025415
    test1_2_0: use ..: 0.025523

    test1_3_0: use table.concat: 0.035074
    test1_3_0: use table.concat: 0.035227
    test1_3_0: use table.concat: 0.035077
    test1_3_0: use table.concat: 0.034965
    test1_3_0: use table.concat: 0.035284
  • 考虑到这边测试的拼接的都是本身就是字符串的变量,日常中会有很多将number拼接的情况,修改测试函数,使用纯number拼接测试一次:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    -- string.format
    function test1_1_0()
    io.write("test1_1_0: use string.format: ")
    for i=1, testCount do
    local str = string.format("%d-string1-%d-string2-%d",
    math.random(1, randomStringSzie),
    math.random(1, randomStringSzie),
    math.random(1, randomStringSzie))
    end
    end

    -- ..
    function test1_2_0()
    io.write("test1_2_0: use ..: ")
    for i=1, testCount do
    local str = math.random(1, randomStringSzie) .. "-string1-" ..
    math.random(1, randomStringSzie) .. "-string2-" ..
    math.random(1, randomStringSzie)
    end
    end

    -- table.concat
    function test1_3_0()
    io.write("test1_3_0: use table.concat: ")
    for i=1, testCount do
    local str = table.concat({math.random(1, randomStringSzie), "-string1-",
    math.random(1, randomStringSzie), "-string2-",
    math.random(1, randomStringSzie)})
    end
    end
  • 同样的方式测试5次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    test1_1_0: use string.format: 0.028289
    test1_1_0: use string.format: 0.02815
    test1_1_0: use string.format: 0.028763
    test1_1_0: use string.format: 0.028143
    test1_1_0: use string.format: 0.028161

    test1_2_0: use ..: 0.031198
    test1_2_0: use ..: 0.030967
    test1_2_0: use ..: 0.031142
    test1_2_0: use ..: 0.031129
    test1_2_0: use ..: 0.03098

    test1_3_0: use table.concat: 0.038446
    test1_3_0: use table.concat: 0.037694
    test1_3_0: use table.concat: 0.037729
    test1_3_0: use table.concat: 0.03787
    test1_3_0: use table.concat: 0.037877
  • 测试结论

    • 日常使用情况下都不应该使用table.concat函数来进行拼接字符串,因为每次拼接都会有创建table的性能消耗,并且也会带来gc压力
    • 如果使用number比较多的情况下的拼接的字符串,使用string.format性能比..好,反之,如果number较少使用..性能更好;测试发现,日常情况下(总拼接参数个数小于8个),如果number数量:string数量<=1的情况下,使用..性能会好点,其他情况下使用string.format

特殊情况——已存在table拼接

将一个已存在的table中的字符串拼接也可以用这三种字符串进行拼接,那table的大小会不会对三种方式性能有影响?

  • 测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    if jit then
    jit.off()
    end

    math.randomseed(os.time())

    local strTable = {}
    function makeStrTable()
    for i=1,200000 do
    strTable[#strTable + 1] = tostring(math.random(1,10000000))
    end
    end

    local maxCount = 50000

    function test2_1(arr, len)
    io.write("test2_1: ")

    local start = os.clock()

    for cnt=1, maxCount do
    local str = ""
    for i=1, len do
    str = string.format("%s%s", str, arr[i])
    end
    end

    print( os.clock() - start)
    end

    function test2_2(arr, len)
    io.write("test2_2: ")

    local start = os.clock()

    for cnt=1, maxCount do
    local str = ""
    for i=1, len do
    str = str..arr[i]
    end
    end

    print( os.clock() - start)
    end

    function test2_3(arr, len)
    io.write("test2_3: ")

    local start = os.clock()

    for cnt=1, maxCount do
    local str = table.concat(arr)
    end

    print( os.clock() - start)
    end

    makeStrTable()

    for i = 2, 10 do
    local concatTable = {}
    for j=1, i do
    concatTable[#concatTable + 1] = strTable[math.random(1,200000)]
    end
    collectgarbage("collect")
    test2_1(concatTable, i)
    collectgarbage("collect")
    test2_2(concatTable, i)
    collectgarbage("collect")
    test2_3(concatTable, i)
    end
  • 同样为了相互直接的影响,分别注释并测试的测试结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    test2_1: 0.012331
    test2_1: 0.018489
    test2_1: 0.02484
    test2_1: 0.030889
    test2_1: 0.03711
    test2_1: 0.044915
    test2_1: 0.050445
    test2_1: 0.057397
    test2_1: 0.064486

    test2_2: 0.007221
    test2_2: 0.011114
    test2_2: 0.013879
    test2_2: 0.017953
    test2_2: 0.021438
    test2_2: 0.024831
    test2_2: 0.028542
    test2_2: 0.033702
    test2_2: 0.037664

    test2_3: 0.00487
    test2_3: 0.005351
    test2_3: 0.005765
    test2_3: 0.006716
    test2_3: 0.007291
    test2_3: 0.00796
    test2_3: 0.0082739999999999
    test2_3: 0.009553
    test2_3: 0.010339
  • 测试结论
    • 在已经存在的table去调用字符串拼接,不论table里面元素多少,都是table.concat快,因为没有了table创建的开销

特殊情况——创建一个table并拼接

一般情况下,都是从某个table选取一些进行拼接,这时候就不能直接用table.concat对已存在的table进行拼接,需要重新构造一个新的table,或者直接使用string.format, .. 进行拼接,那这种情况下,使用哪种性能最好?新构造的table的大小对各种情况有没有性能影响?

  • 测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    if jit then
    jit.off()
    end

    math.randomseed(os.time())

    local strTable = {}
    function makeStrTable()
    for i=1,200000 do
    strTable[#strTable + 1] = tostring(math.random(1,10000000))
    end
    end

    local maxCount = 50000

    function test2_1(arr, len)
    io.write("test2_1: len: " .. len .. ": ")

    local start = os.clock()

    for cnt=1, maxCount do
    local str = ""
    for i=1, len do
    str = string.format("%s%s", str, arr[i])
    end
    end

    print(os.clock() - start)
    end

    function test2_2(arr, len)
    io.write("test2_2: len: " .. len .. ": ")

    local start = os.clock()

    for cnt=1, maxCount do
    local str = ""
    for i=1, len do
    str = str..arr[i]
    end
    end

    print(os.clock() - start)
    end

    function test2_3(arr, len)
    io.write("test2_3: len: " .. len .. ": ")

    local start = os.clock()

    for cnt=1, maxCount do
    local tab = {}
    for i=1, len do
    tab[#tab+1] = arr[i]
    end

    local str = table.concat(tab)
    end

    print(os.clock() - start)
    end

    makeStrTable()

    for i = 2, 50 do
    local concatTable = {}
    for j=1, i do
    concatTable[#concatTable + 1] = strTable[math.random(1,200000)]
    end
    collectgarbage("collect")
    test2_1(concatTable, i)
    collectgarbage("collect")
    test2_2(concatTable, i)
    collectgarbage("collect")
    test2_3(concatTable, i)
    end
  • 因为要测试的比较多,这里直接忽略函数之间的影响,直接获取跑的结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    test2_1: len: 2: 0.012323
    test2_2: len: 2: 0.007064
    test2_3: len: 2: 0.017743
    test2_1: len: 3: 0.018373
    test2_2: len: 3: 0.010368
    test2_3: len: 3: 0.027305
    test2_1: len: 4: 0.024554
    test2_2: len: 4: 0.013748
    test2_3: len: 4: 0.026225
    test2_1: len: 5: 0.027159
    test2_2: len: 5: 0.015262
    test2_3: len: 5: 0.035428
    test2_1: len: 6: 0.032727
    test2_2: len: 6: 0.018682
    test2_3: len: 6: 0.036837
    test2_1: len: 7: 0.038253
    test2_2: len: 7: 0.021658
    test2_3: len: 7: 0.038941
    test2_1: len: 8: 0.043715
    test2_2: len: 8: 0.024652
    test2_3: len: 8: 0.041066
    test2_1: len: 9: 0.050002
    test2_2: len: 9: 0.028612
    test2_3: len: 9: 0.054813
    test2_1: len: 10: 0.05554
    test2_2: len: 10: 0.031326
    test2_3: len: 10: 0.055422
    test2_1: len: 11: 0.062763
    test2_2: len: 11: 0.036097
    test2_3: len: 11: 0.057184
    test2_1: len: 12: 0.069464
    test2_2: len: 12: 0.040439
    test2_3: len: 12: 0.060116
    test2_1: len: 13: 0.075878
    test2_2: len: 13: 0.044668
    test2_3: len: 13: 0.062267
    test2_1: len: 14: 0.082757
    test2_2: len: 14: 0.049663
    test2_3: len: 14: 0.065098
    test2_1: len: 15: 0.089878
    test2_2: len: 15: 0.052364
    test2_3: len: 15: 0.067282
    test2_1: len: 16: 0.096886
    test2_2: len: 16: 0.055969
    test2_3: len: 16: 0.071199
    test2_1: len: 17: 0.105697
    test2_2: len: 17: 0.058038
    test2_3: len: 17: 0.092911
    test2_1: len: 18: 0.107923
    test2_2: len: 18: 0.063079
    test2_3: len: 18: 0.093996
    test2_1: len: 19: 0.11523
    test2_2: len: 19: 0.067008
    test2_3: len: 19: 0.097838
    test2_1: len: 20: 0.121543
    test2_2: len: 20: 0.070144
    test2_3: len: 20: 0.095648
    test2_1: len: 21: 0.125822
    test2_2: len: 21: 0.074236
    test2_3: len: 21: 0.097661
    test2_1: len: 22: 0.133389
    test2_2: len: 22: 0.079203
    test2_3: len: 22: 0.10079
    test2_1: len: 23: 0.141318
    test2_2: len: 23: 0.084614999999999
    test2_3: len: 23: 0.104983
    test2_1: len: 24: 0.148233
    test2_2: len: 24: 0.091413
    test2_3: len: 24: 0.107491
    test2_1: len: 25: 0.155905
    test2_2: len: 25: 0.093782
    test2_3: len: 25: 0.110824
    test2_1: len: 26: 0.16504
    test2_2: len: 26: 0.105242
    test2_3: len: 26: 0.116656
    test2_1: len: 27: 0.180588
    test2_2: len: 27: 0.107747
    test2_3: len: 27: 0.115238
    test2_1: len: 28: 0.179846
    test2_2: len: 28: 0.111915
    test2_3: len: 28: 0.117459
    test2_1: len: 29: 0.190444
    test2_2: len: 29: 0.122584
    test2_3: len: 29: 0.120503
    test2_1: len: 30: 0.200754
    test2_2: len: 30: 0.125184
    test2_3: len: 30: 0.122902
    test2_1: len: 31: 0.206196
    test2_2: len: 31: 0.132831
    test2_3: len: 31: 0.12567
    test2_1: len: 32: 0.212643
    test2_2: len: 32: 0.13621
    test2_3: len: 32: 0.128972
    test2_1: len: 33: 0.224638
    test2_2: len: 33: 0.142706
    test2_3: len: 33: 0.155577
    test2_1: len: 34: 0.230299
    test2_2: len: 34: 0.151168
    test2_3: len: 34: 0.159434
    test2_1: len: 35: 0.24495
    test2_2: len: 35: 0.156305
    test2_3: len: 35: 0.164588
    test2_1: len: 36: 0.259361
    test2_2: len: 36: 0.174812
    test2_3: len: 36: 0.172479
    test2_1: len: 37: 0.268614
    test2_2: len: 37: 0.174493
    test2_3: len: 37: 0.175878
    test2_1: len: 38: 0.280801
    test2_2: len: 38: 0.181204
    test2_3: len: 38: 0.180053
    test2_1: len: 39: 0.286958
    test2_2: len: 39: 0.189557
    test2_3: len: 39: 0.182902
    test2_1: len: 40: 0.296747
    test2_2: len: 40: 0.190833
    test2_3: len: 40: 0.177761
    test2_1: len: 41: 0.298487
    test2_2: len: 41: 0.198188
    test2_3: len: 41: 0.181526
    test2_1: len: 42: 0.307086
    test2_2: len: 42: 0.201846
    test2_3: len: 42: 0.186407
    test2_1: len: 43: 0.318794
    test2_2: len: 43: 0.208237
    test2_3: len: 43: 0.186203
    test2_1: len: 44: 0.323743
    test2_2: len: 44: 0.221747
    test2_3: len: 44: 0.199269
    test2_1: len: 45: 0.346715
    test2_2: len: 45: 0.233323
    test2_3: len: 45: 0.199532
    test2_1: len: 46: 0.360915
    test2_2: len: 46: 0.236748
    test2_3: len: 46: 0.196382
    test2_1: len: 47: 0.360939
    test2_2: len: 47: 0.236785
    test2_3: len: 47: 0.197901
    test2_1: len: 48: 0.364207
    test2_2: len: 48: 0.243074
    test2_3: len: 48: 0.201713
    test2_1: len: 49: 0.373341
    test2_2: len: 49: 0.25071
    test2_3: len: 49: 0.203139
    test2_1: len: 50: 0.383352
    test2_2: len: 50: 0.262008
    test2_3: len: 50: 0.207223
  • 测试结论
    • 可以看到..的性能一直会高于string.format(这里假设table里面的元素都是字符串的情况下,如果为全数字的情况经过测试string.format性能高于..)
    • 当table长度在29的时候(全数字情况下测试为36),table.concat性能最好,这个数字不一定准确,但是可以大致了解下这个分界值,而且使用string.format与..会带来后续gc的性能影响(这个也比较难测),所以可以大致把这个值估计在25(纯数字的情况下,可以稍微再高点),如果超过这个值,就是用table.concat会更优,写起来也会更加方便