【问题标题】:Can you preallocate space for a String in Julia?您可以在 Julia 中为字符串预分配空间吗?
【发布时间】:2017-03-19 20:09:32
【问题描述】:

Julia 文档描述了 pre-allocating memory 的 Array 以通过避免垃圾收集来提高性能。毕竟,这可能与作为向量的String 有关吗? String源代码为here。

我的用例是我正在处理大型文本转储,使用readuntil() 获取一个块,然后执行正则表达式match() 或matchall() 或replace()。我已经用 Perl 编写了代码,但想看看 Julia 是否可以更快。我已经知道我必须处理的最长字符串的长度。

fs=open(fn,"r")
    while !eof(fs)
        text = readuntil(fs, "</tag>")
        text = match(r"pattern"s, text).match
        text = replace(text, r"badpattern", "goodpattern")
        text = replace(text, r"anotherbadpattern", "betterpattern")
        ... (dozens more replacements)
    end
close(fs)

我预计磁盘 I/O 将成为主要瓶颈,但我有兴趣了解任何有用的东西。我欢迎任何关于加快进程的可能方法的建议。

【问题讨论】:

  • 您链接到的源代码是String 的构造函数,它以Vector 作为输入,但它不是类型定义本身(正如@slowbrain 指出的那样,它是不可变)。
  • 你能添加更具体的代码示例吗?
  • @DanGetz 添加了代码示例。我希望不必(重新)分配 text 字符串数百万次,因为有很多循环来收集 ed 文本和许多替换。我希望使用 Julia 的正则表达式功能,因为我已经在 Perl 中对此进行了编码,但正如 @ColinTBowers 和 @slowbrain 所指出的,String 是不可变的,因此无法重新分配。
  • 你能给我们比较 Perl 和 Julia 版本的时间吗?我的答案中的代码将replace 的分配减少了 50%。原来text 不是唯一分配的东西。你知道 Perl 是否进行分配吗?

标签: arrays string performance memory-management julia


【解决方案1】:

Julia 中的字符串是不可变的,因此预分配的概念不起作用。

julia> a = "aaaa";
julia> pointer(a)
   Ptr{UInt8} @0x0000000119628f50

julia> a = "bbbb";
julia> pointer(a)
   Ptr{UInt8} @0x000000011963a030

【讨论】:

    【解决方案2】:

    首先,

    “过早的优化是万恶之源”-- Donald Knuth

    话虽如此,Base 中的 replace 代码显示它读取源字符串并找到要替换的匹配项,并将结果字符串的一部分写入 IOBuffer。处理后它从 IOBuffer 中读取输出字符串。本质上,这会复制源字符串的两个副本。如果输入字符串(以及输出)的最大长度已知,replace 中的 IOBuffer 可以被重用而不是分配。

    要将此分析放入代码中,请定义以下replace!(基于replace):

    function replace!(out::IOBuffer, str::String, pattern, repl, limit::Integer)
      n = 1
      e = endof(str)
      i = a = start(str)
      r = search(str,pattern,i)
      j, k = first(r), last(r)
      Base.ensureroom(out, floor(Int, 1.2sizeof(str)))
      while j != 0
        if i == a || i <= k
          Base.write_sub(out, str.data, i, j-i)
          Base._replace(out, repl, str, r, pattern)
        end
        if k<j
          i = j
          k = nextind(str, j)
        else
          i = k = nextind(str, k)
        end
        if j > e
          break
        end
        r = search(str,pattern,k)
        j, k = first(r), last(r)
        n == limit && break
        n += 1
      end
      write(out, SubString(str,i))
      takebuf_string(out)
    end
    
    replace!(out::IOBuffer,s::AbstractString, pat, f, n::Integer) =
      replace!(out, String(s), pat, f, n)
    replace!(out::IOBuffer,s::AbstractString, pat, r) =
      replace!(out, s, pat, r, 0)
    

    现在,

    const string1 = "the quick brown fox jumped over the lazy dog"
    const out = IOBuffer(100)
    
    replace!(out,string1,"quick","slow")
    

    将执行替换而不分配 IOBuffer。在replace! 之后,out 可以被重用(在replace! 中,takebuf_string 恢复为空。基准测试表明这减少了 50% 的分配并加快了进程。如果有很多 replace 语句已使用,正如您在问题中建议的那样,这应该会带来 30% 或更多的改进。

    如果您尝试这样做,很高兴听到真实世界的基准测试结果。也许应该将某些版本的replace! 放入Base。

    【讨论】:

      【解决方案3】:

      在 Julia 0.6 中,尝试更优化的版本更容易。具体来说,我们可以让 IOBuffer 和 String 共享相同的内存。 replace 函数写入一个 IOBuffer 并处理一个字符串,所以这让我们有机会在没有任何分配的情况下执行 replace!。

      一系列replace! 语句可以在两个预先分配的缓冲区之间交替,并且(几乎)避免任何分配。

      更具体地说:

      # version 0.6.0-pre.alpha.220
      function replace!(out::IOBuffer, str::AbstractString, 
        pattern, repl, limit::Integer)
          n = 1
          e = endof(str)
          i = a = start(str)
          r = search(str,pattern,i)
          j, k = first(r), last(r)
          out.size = 0
          out.ptr = 1
          while j != 0
              if i == a || i <= k
                  Base.unsafe_write(out, pointer(str, i), UInt(j-i))
                  Base._replace(out, repl, str, r, pattern)
              end
              if k<j
                  i = j
                  k = nextind(str, j)
              else
                  i = k = nextind(str, k)
              end
              if j > e
                  break
              end
              r = search(str,pattern,k)
              j, k = first(r), last(r)
              n == limit && break
              n += 1
          end
          write(out, SubString(str,i))
        return out.ptr-1
      end
      

      为了驱动这个函数并预分配缓冲区,我们这样做:

      const string1 = "."^1000
      const string2 = "."^1000
      
      const io1 = IOBuffer(convert(Vector{UInt8},string1),true,true,length(string1))
      const io2 = IOBuffer(convert(Vector{UInt8},string2),true,true,length(string2))
      
      write(io1,"hello world. "^30)   # test string to manipulate
      
      function switcheroo!()
          replace!(io2,SubString(string1,1,io1.ptr-1),"hello","what's up",0)
          replace!(io1,SubString(string2,1,io2.ptr-1),"what's up","hello",0)
      end
      
      switcheroo!()
      

      函数switcheroo! 将hello 替换为whats up 并再次返回。正如问题所暗示的那样,修改代码以处理一系列replace 很简单。

      注意事项:

      1. 这与 Julia String 内部更紧密地结合在一起,因此在未来的 Julia 版本中可能会不稳定(但可能会保留有效的替代方案)。

      2. 对于创建的两个 SubString 值,每个 replace! 仍有两个(讨厌的!)分配。这些分配是 64 字节,不会随着字符串大小而增长。

      【讨论】:

      • 你的速度对我的编程能力来说太快了!你的两个答案都很棒。我仍在测试您之前创建的replace!(),并试图找出可比较的 Perl 时序并考虑磁盘访问。我认为在 v0.6 发布之前我不会接触到这个 ;)
      猜你喜欢
      • 2018-04-07
      • 2015-03-24
      • 2019-03-01
      • 1970-01-01
      • 2010-12-07
      • 2016-02-16
      • 1970-01-01
      • 2023-03-28
      • 1970-01-01
      相关资源
      最近更新 更多