【问题标题】:Ruby - Array.join versus String Concatenation (Efficiency)Ruby - Array.join 与字符串连接(效率)
【发布时间】:2011-05-23 02:05:21
【问题描述】:

我记得曾经因为在 Python 中连接字符串而受到责骂。有人告诉我,在 Python 中创建一个字符串列表并在以后加入它们会更有效。我将这种做法带到了 JavaScript 和 Ruby 中,尽管我不确定这在后者中是否有同样的好处。

谁能告诉我加入一个字符串数组并在它们上调用 :join 或在 Ruby 编程语言中根据需要连接一个字符串是否更有效(在资源和执行方面)?

谢谢。

【问题讨论】:

    标签: ruby


    【解决方案1】:

    使用Benchmark 类自己尝试一下。

    require "benchmark"
    
    n = 1000000
    Benchmark.bmbm do |x|
      x.report("concatenation") do
        foo = ""
        n.times do
          foo << "foobar"
        end
      end
    
      x.report("using lists") do
        foo = []
        n.times do
          foo << "foobar"
        end
        string = foo.join
      end
    end
    

    这会产生以下输出:

    Rehearsal -------------------------------------------------
    concatenation   0.300000   0.010000   0.310000 (  0.317457)
    using lists     0.380000   0.050000   0.430000 (  0.442691)
    ---------------------------------------- total: 0.740000sec
    
                        user     system      total        real
    concatenation   0.260000   0.010000   0.270000 (  0.309520)
    using lists     0.310000   0.020000   0.330000 (  0.363102)
    

    所以在这种情况下,连接看起来要快一些。针对您的用例在您的系统上进行基准测试。

    【讨论】:

    • +1 基准!基准!基准!基准是我们的朋友! :-)
    • 这非常令人惊讶 - 在 Pickaxe 中,他们将使用列表描述为更快。如果内存有限,结果可能会有所不同。
    • 它可能在不同的环境、不同的数据大小和不同的 Ruby 版本中有所不同。基准测试和优化有时可能是黑魔法。
    • 请注意,如果数组已经构建好了,执行ary.join 将比遍历数组并连接它们更快。
    【解决方案2】:

    有趣的是,基准测试给出了令人惊讶的结果(除非我做错了什么):

    require 'benchmark'
    
    N = 1_000_000
    Benchmark.bm(20) do |rep|
    
      rep.report('+') do
        N.times do
          res = 'foo' + 'bar' + 'baz'
        end
      end
    
      rep.report('join') do
        N.times do
          res = ['foo', 'bar', 'baz'].join
        end
      end
    
      rep.report('<<') do
        N.times do
          res = 'foo' << 'bar' << 'baz'
        end
      end
    end
    

    给予

    jablan@poneti:~/dev/rb$ ruby concat.rb 
                              user     system      total        real
    +                     1.760000   0.000000   1.760000 (  1.791334)
    join                  2.410000   0.000000   2.410000 (  2.412974)
    <<                    1.380000   0.000000   1.380000 (  1.376663)
    

    join 结果是最慢的。它可能与创建数组有关,但无论如何你都必须这样做。

    哦,顺便说一句,

    jablan@poneti:~/dev/rb$ ruby -v
    ruby 1.9.1p378 (2010-01-10 revision 26273) [i486-linux]
    

    【讨论】:

    • 我认为差异可能是由于您在循环中创建了大量的小数组,而不是从数组中构建一个长字符串。您的基准和上面的基准正在测试不同的场景。
    • 是的。恕我直言,在大多数实际情况下,我们连接的是少数字符串,而不是数百万。
    【解决方案3】:

    是的,原理是一样的。我记得一个 ProjectEuler 谜题,我尝试了两种方式,调用 join 要快得多。

    如果你查看 Ruby 源代码,join 全部用 C 实现,它会比连接字符串快很多(没有中间对象创建,没有垃圾收集):

    /*
     *  call-seq:
     *     array.join(sep=$,)    -> str
     *  
     *  Returns a string created by converting each element of the array to
     *  a string, separated by <i>sep</i>.
     *     
     *     [ "a", "b", "c" ].join        #=> "abc"
     *     [ "a", "b", "c" ].join("-")   #=> "a-b-c"
     */
    
    static VALUE
    rb_ary_join_m(argc, argv, ary)
        int argc;
        VALUE *argv;
        VALUE ary;
    {
        VALUE sep;
    
        rb_scan_args(argc, argv, "01", &sep);
        if (NIL_P(sep)) sep = rb_output_fs;
    
        return rb_ary_join(ary, sep);
    }
    

    rb_ary_join 在哪里:

     VALUE rb_ary_join(ary, sep)
         VALUE ary, sep;
     {
         long len = 1, i;
         int taint = Qfalse;
         VALUE result, tmp;
    
         if (RARRAY(ary)->len == 0) return rb_str_new(0, 0);
         if (OBJ_TAINTED(ary) || OBJ_TAINTED(sep)) taint = Qtrue;
    
         for (i=0; i<RARRAY(ary)->len; i++) {
         tmp = rb_check_string_type(RARRAY(ary)->ptr[i]);
         len += NIL_P(tmp) ? 10 : RSTRING(tmp)->len;
         }
         if (!NIL_P(sep)) {
         StringValue(sep);
         len += RSTRING(sep)->len * (RARRAY(ary)->len - 1);
         }
         result = rb_str_buf_new(len);
         for (i=0; i<RARRAY(ary)->len; i++) {
         tmp = RARRAY(ary)->ptr[i];
         switch (TYPE(tmp)) {
           case T_STRING:
             break;
           case T_ARRAY:
             if (tmp == ary || rb_inspecting_p(tmp)) {
             tmp = rb_str_new2("[...]");
             }
             else {
             VALUE args[2];
    
             args[0] = tmp;
             args[1] = sep;
             tmp = rb_protect_inspect(inspect_join, ary, (VALUE)args);
             }
             break;
           default:
             tmp = rb_obj_as_string(tmp);
         }
         if (i > 0 && !NIL_P(sep))
             rb_str_buf_append(result, sep);
         rb_str_buf_append(result, tmp);
         if (OBJ_TAINTED(tmp)) taint = Qtrue;
         }
    
         if (taint) OBJ_TAINT(result);
         return result;
    }
    

    【讨论】:

    • 很有趣,但@Mladen Jablanović 基准测试显示它更慢。
    • @Mladen Jablanović 在他的基准测试中间创建了两个字符串和一个数组......这个答案只是在谈论 only join 方法——假设那些东西已经存在了。
    【解决方案4】:

    我刚刚读到这个。 Attahced 是一个谈论它的链接。

    Building-a-String-from-Parts

    据我了解,在 Python 和 Java 中,字符串是与数组不同的不可变对象,而在 Ruby 中,字符串和数组彼此一样是可变的。使用 String.concat 或

    我认为该链接会比我更好地解释这一点。

    谢谢,

    马丁

    【讨论】:

    • 这个答案对我最有用(特别是链接),但我必须选择基准作为最准确的答案。
    【解决方案5】:

    " 问题在于整个数据堆。在他的第一种情况下,他有两种类型的数据存储:(1) CSV 文件中每一行的临时字符串,带有固定的引号等内容,以及 (2) 包含所有内容的巨大字符串。如果每个字符串是 1k 并且有 5,000 行...

    场景一:从小字符串构建大字符串

    临时字符串:5 兆 (5,000k) 大弦:5 兆 (5,000k) 总计:10 兆(10,000k) Dave 的改进脚本将大量字符串替换为数组。他保留了临时字符串,但将它们存储在一个数组中。该数组最终只会花费 5000 * sizeof(VALUE) 而不是每个字符串的完整大小。通常,一个 VALUE 是四个字节。

    场景二:将字符串存储在数组中

    字符串:5 兆 (5,000k) 海量数组:20k

    然后,当我们需要制作一个大字符串时,我们调用join。现在我们达到了十兆,突然所有这些字符串都变成了临时字符串,它们都可以立即释放。最终这是一个巨大的成本,但它比一个逐渐增加的、一直在消耗资源的渐强效率要高得多。 "

    http://viewsourcecode.org/why/hacking/theFullyUpturnedBin.html

    ^实际上最好在内存/垃圾收集性能中将操作延迟到最后,就像我在 Python 中被教导的那样。原因开始于您在最后获得一大块分配和对象的即时释放。

    【讨论】:

      【解决方案6】:

      @jergason 的answer 显示连接稍快,但这是because the shovel operator &lt;&lt; is allowed to modify the original string

      如果我们在顶部使用frozen_string_literal: true 运行相同的基准测试,您会得到以下结果:

      Rehearsal -------------------------------------------------
      using lists     0.140621   0.015146   0.155767 (  0.308191)
      concatenation Traceback (most recent call last):
          8: from main.rb:5:in `<main>'
          7: from /usr/lib/ruby/2.5.0/benchmark.rb:255:in `bmbm'
          6: from /usr/lib/ruby/2.5.0/benchmark.rb:255:in `inject'
          5: from /usr/lib/ruby/2.5.0/benchmark.rb:255:in `each'
          4: from /usr/lib/ruby/2.5.0/benchmark.rb:257:in `block in bmbm'
          3: from /usr/lib/ruby/2.5.0/benchmark.rb:293:in `measure'
          2: from main.rb:16:in `block (2 levels) in <main>'
          1: from main.rb:16:in `times'
      main.rb:17:in `block (3 levels) in <main>': can't modify frozen String (FrozenError)
      

      如果您更新串联基准以使用+= 而不是&lt;&lt;,您会发现串联基准永远不会终止。

      因此,Array#join 比多次调用+= 更快。

      【讨论】:

        猜你喜欢
        • 2020-01-21
        • 2012-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-27
        • 2017-04-21
        • 2013-09-17
        相关资源
        最近更新 更多