10秒钟内可获得27亿积分
当我阅读有关在Macbook上在一分钟内可视化27亿个点的博客文章时 ,我很好奇这在Julia中有多快。
不仅是为了娱乐,而且将其集成到我的绘图库( MakiE )中。 由于我对能够快速创建快速解决方案感到非常高兴,因此我决定分享自己的经验!
首先,您可以从http://planet.osm.org/gps/下载数据
解析CSV
我假设有人想对数据做很多操作,所以最明智的做法是将55gb CSV转换为22gb二进制blob,它可以获取内存映射并且不需要解析!
周围没有任何工具可以开箱即用,但是借助TextParser .jl,我能够开发出一种相当快速的自定义解决方案-这也演示了如何很好地扩展现有的Julia库。
第一个问题是,TextParser需要一个字符串来解析CSV。 由于不能将整个55gb数据集加载到RAM中,因此我们的第一个技巧是在Julia中创建一个内存映射的字符串类型:
struct MMapString{T}
# using Type parameter to not write out the result type of mmap
# but I also don't want to loose performance
x::T
end
# Constructor from a path
MMapString(path::String) = MMapString(Mmap.mmap(open(path), Vector{UInt8}))
# we only need to overload the iteration protocol for TextParse to work!
Base.getindex(x::MMapString, i) = Char(x.x[i])
Base.next(x::MMapString, i) = Char(x.x[i]), i + 1
Base.done(x::MMapString, i) = i > length(x.x)
Base.start(x::MMapString, i) = 1
Base.length(x::MMapString) = length(x.x)
这是满足大多数需要类似字符串类型的库的基本接口所需要的。 现在我们可以编写一个函数来读取55gb数据集,逐行解析并将其写入二进制blob。
发生的问题之一是,写入IO文件流的函数分配了很多东西。 Base.write为其参数创建一个可变容器(Ref),以将其安全地传递给C。即使直接采用指针的Base.unsafe_write仍然具有分配128字节的检查。
准备好unsafe_unsafe_write ,简短的uuwrite 。 这样可以节省40秒,并减少3.5 gb的内存(最终速度提高约1.2倍)!
这实际上是一个很好的例子,说明了朱莉娅目前是如何失败的,以及如何仍然可以收回表现。
我们只是直接调用C库,并使用几乎不赞成使用的语法(`&`)从堆栈分配的参数中获取指针。
在茱莉亚0.7,这是可能没有问题,我们应该能够只使用`` 写 ,因为编译器在堆栈中分配,并消除可变容器有很多更好。
顺便说一句,我正在使用@time write(io,1f0)来显示分配情况,然后使用@which write(io,1f0) (或@edit直接跳转到编辑器) 来找出问题所在。函数是在Julia中定义的以及分配的来源。
using TextParse
using TextParse: Record, Field, Numeric, tryparsenext
function uuwrite(io, ptr::T) where T
Int(ccall(
:ios_write,
Csize_t, (Ptr{Void}, Ptr{T}, Csize_t),
io.ios, &ptr, sizeof(T))
)
end
function save2bin(path, n = typemax(Int))
str = MMapString(path)
# Descriptor of 'num,num\n' which is the format in the csv
rec = Record((
Field(Numeric(Float32), delim = ','),
Field(Numeric(Float32), eoldelim = true)
))
# skip the header! Nice is, that Julia's findfirst works with
# any iterator
pos = findfirst(str, '\n') + 1
io = open(homedir()*"/gpspoints.bin", "w")
i = 0
while !done(str, pos) && i <= n
# tryparsenext takes roughly 35ns so it's fairly optimal
p_or_null, pos = tryparsenext(
rec, str, pos, length(str)
)
isnull(p_or_null) && continue # continue when parsing fails
p = get(p_or_null)
uuwrite(io, p)
i += 1
end
close(io)
i
end
tic()
@time save2bin(homedir() * "/Downloads/gps-points.csv");
toc()
这将重写文件,并在190秒内将其保存为二进制文件,对于这样简单的实现和只需要执行一次的操作来说,这还不错。
绘制图像
现在,我们可以将文件存储为Vector {NTuple {2,Float32}}映射,其中包含原始gps坐标。 然后,我们可以直接在这些点上循环并累积2D数组中的每个位置。
"""
Transforms from longitude/latitude to pixel on screen, with `dims` refering to
the dimensions of the screen in pixel
"""
@inline function gps2pixel(point, dims)
lon, lat = point[1], point[2]
x = ((dims[1] / 180.0) * (90.0 + (lon / 10^7)))
y = ((dims[2] / 360.0) * (180.0 - (lat / 10^7)))
(x, y)
end
function to_image_inner!(img, points, start, stop)
dims = size(img)
for i = start:stop
# don't check bounds when indexing into an array
@inbounds begin
p0 = gps2pixel(points[i], dims)
idx = Int.(round.(p0))
xidx, yidx = dims[1] - idx[1], dims[2] - idx[2]
if checkbounds(Bool, img, xidx, yidx)
# we should give each point a radius and then add
# the coverage to each pixel for a smoother image
# But this does well enough for this short example:
img[xidx, yidx] += 0.001f0 # accumulate
end
end
end
end
function to_image!(img, points, range)
N = length(range)
# Number of threads Julia has available
# Use the environment variable JULIA_NUM_THREADS=8 to start
# Julia with 8 threads
NT = Threads.nthreads()
slices = floor(Int, N / NT)
offset = minimum(range)
Threads. @threads for i = 1:NT
# @threads creates a closure, which sometimes
# confuses type inference leading to slow code.
# this is why it's a good practice to move the loop body
# behind a function barrier
# ( https://docs.julialang.org/en/latest/manual/performance-tips/#kernal-functions-1 )
to_image_inner!(
img, points,
offset + ((i-1) * slices + 1), offset + (i * slices)
)
end
return img
end
我们可以通过以下方式使用上述功能来获得简单的灰度图像:
img = zeros(Float32, 600, 960)
io = open(homedir() * "/gpspoints.bin")
points = Mmap.mmap(io, Vector{NTuple{2, Float32}})
tic()
to_image!(img, points, 1:length(points))
toc()
using FileIO, Colors # save it with FileIO as a Gray image
FileIO.save("gps.png", Gray.(clamp.(1f0 .- img, 0, 1)))
瞧!
在普通台式计算机上花了10秒钟绘制了22gb的点。
需要指出的一件事是您可以在示例中看到的出色的点调用语法:
Gray.(clamp.(1f0 .- img, 0, 1)))
实际上,这是一种很棒的机制, 可以在融合所有操作的同时在数组上的每个元素上应用功能 。 所以上面将变成一个for循环
一次性完成img ,夹紧,反转并将其转换为颜色的操作!
我们还可以使用新的交互式绘图库为进度创建漂亮的动画:
using MakiE
resolution = (600, 960)
scene = Scene(resolution = reverse(resolution))
img = zeros(Float32, resolution)
imviz = heatmap(img, colornorm = (0, 1))
center!(scene)
# slice `points` into multiple batches which we will update
slice = 10^7
range = slice:slice:length(points)
stop = 1
vio = VideoStream(scene, homedir()*"/Desktop/", "gps")
while true
start = stop
stop = min(stop + slice, length(points))
to_image!(img, points, start:stop)
imviz[:heatmap] = img # update heatmap plot
recordframe!(vio) # add frame to stream
stop == length(points) && break
end
finish(vio, "gif") # finish streaming and export as gif!
当然,我们可以应用各种改进,但是作为第一个结果,我对此感到非常满意。 还不清楚该解决方案与最初提到的博客文章是否具有可比性,因为我尚未在计算机上运行它。 如果要在计算机上尝试此操作,请查看完整代码 !
From: https://hackernoon.com/drawing-2-7-billion-points-in-10s-ecc8c85ca8fa