你可以在朱莉娅预先为一个字符串分配空间吗?

时间:2017-03-19 20:09:32

标签: arrays string performance memory-management julia

Julia文档为Array描述了pre-allocating memory,以通过避免垃圾回收来提高性能。毕竟这可能与String这是一个Vector有关吗? 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成为主要的瓶颈,但我有兴趣了解任何有用的东西。我欢迎任何关于加快这一进程的可能方法的建议。

3 个答案:

答案 0 :(得分:5)

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

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

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

答案 1 :(得分:3)

首先,

  

“过早优化是所有邪恶的根源” - Donald Knuth

话虽如此,Base中的replace代码显示它读取源String并找到要替换的匹配,并将结果String的部分写入IOBuffer。处理后,它从IOBuffer读取输出String。基本上,这会执行源String的两个副本。如果已知输入字符串的最大长度(以及输出),则可以重用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可以重复使用(takebuf_string中的replace!将其重置为空。基准测试显示这会将分配减少50%并加快流程速度如果使用了很多replace语句,正如您在问题中所建议的那样,这应该会提高30%或更多。

如果你试试这个,听听现实世界的基准测试结果会很高兴。也许某些版本的replace!应放入Base。

答案 2 :(得分:1)

在Julia 0.6中,尝试更优化的版本更容易。具体来说,我们可以让IOBuffer和String共享相同的内存。 replace函数写入IOBuffer并处理String,因此这使我们有机会在没有任何分配的情况下执行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!仍有两个(pesky!)分配。这些分配是64字节,不会随字符串大小增长。