【问题标题】:What are the reasons for this benchmark result?这个基准测试结果的原因是什么?
【发布时间】:2015-04-20 08:30:51
【问题描述】:

将 rgb 图像转换为灰度图像的两个函数:

function rgb2gray_loop{T<:FloatingPoint}(A::Array{T,3})
  r,c = size(A)
  gray = similar(A,r,c)
  for i = 1:r
    for j = 1:c
      @inbounds gray[i,j] = 0.299*A[i,j,1] + 0.587*A[i,j,2] + 0.114 *A[i,j,3]
    end
  end
  return gray
end

还有:

function rgb2gray_vec{T<:FloatingPoint}(A::Array{T,3})
  gray = similar(A,size(A)[1:2]...)
  gray = 0.299*A[:,:,1] + 0.587*A[:,:,2] + 0.114 *A[:,:,3]
  return gray
end

第一个使用循环,第二个使用向量化。

在对它们进行基准测试时(使用 Benchmark 包),对于不同大小的输入图像,我得到以下结果(f1 是循环版本,f2 是矢量化版本):

A = rand(50,50,3):

| Row | Function | Average     | Relative | Replications |
|-----|----------|-------------|----------|--------------|
| 1   | "f1"     | 3.23746e-5  | 1.0      | 1000         |
| 2   | "f2"     | 0.000160214 | 4.94875  | 1000         |

A = rand(500,500,3):

| Row | Function | Average    | Relative | Replications |
|-----|----------|------------|----------|--------------|
| 1   | "f1"     | 0.00783007 | 1.0      | 100          |
| 2   | "f2"     | 0.0153099  | 1.95527  | 100          |

A = rand(5000,5000,3):

| Row | Function | Average  | Relative | Replications |
|-----|----------|----------|----------|--------------|
| 1   | "f1"     | 1.60534  | 2.56553  | 10           |
| 2   | "f2"     | 0.625734 | 1.0      | 10           |

我希望一个函数比另一个更快(可能是 f1,因为 inbounds 宏)。

但我无法解释,为什么矢量化版本对于较大的图像会变得更快。 这是为什么呢?

【问题讨论】:

  • 我认为向量化版本中的gray = similar(A,size(A)[1:2]...) 语句是不必要的,该语言将直接从第二条语句创建适当的数组大小。不过,这并不能解释为什么矢量化版本变得更快。
  • 题外话,但如果你是using Images,你可以说convert(Array{Gray{Float64}}, A)

标签: loops benchmarking vectorization julia


【解决方案1】:

结果的答案是 Julia 中的多维数组以列优先顺序存储。见Julias Memory Order

固定循环版本,关于列主要顺序(交换内部和外部循环变量):

function rgb2gray_loop{T<:FloatingPoint}(A::Array{T,3})
  r,c = size(A)
  gray = similar(A,r,c)
  for j = 1:c
    for i = 1:r
      @inbounds gray[i,j] = 0.299*A[i,j,1] + 0.587*A[i,j,2] + 0.114 *A[i,j,3]
    end
  end
  return gray
end

A = rand(5000,5000,3) 的新结果:

| Row | Function | Average  | Relative | Replications |
|-----|----------|----------|----------|--------------|
| 1   | "f1"     | 0.107275 | 1.0      | 10           |
| 2   | "f2"     | 0.646872 | 6.03004  | 10           |

以及较小数组的结果:

A = rand(500,500,3):

| Row | Function | Average    | Relative | Replications |
|-----|----------|------------|----------|--------------|
| 1   | "f1"     | 0.00236405 | 1.0      | 100          |
| 2   | "f2"     | 0.0207249  | 8.76671  | 100          |

A = rand(50,50,3):

| Row | Function | Average     | Relative | Replications |
|-----|----------|-------------|----------|--------------|
| 1   | "f1"     | 4.29321e-5  | 1.0      | 1000         |
| 2   | "f2"     | 0.000224518 | 5.22961  | 1000         |

【讨论】:

  • 不错。您是否也可以在循环中尝试 @simd 宏,看看它是否进一步加快了速度?
【解决方案2】:

只是猜测,因为我不认识 Julia-Lang:

我认为向量化形式的语句gray = ... 创建了一个新数组,其中存储了所有计算值,而旧数组被废弃。在f1 中,值被原地覆盖,因此不需要新的内存分配。内存分配非常昂贵,因此具有就地覆盖的循环版本对于低数字更快。

但是内存分配通常是静态开销(分配两倍而不需要两倍的时间)并且矢量化版本的计算速度更快(可能是并行的?)所以如果数字变得足够大,更快的计算会产生更大的差异比内存分配。

【讨论】:

  • 在 Julia 中,向量化操作通常比元素操作慢,因为后者产生的临时性更少。这里的矢量化版本将创建三个临时数组,然后将它们相加,而元素化版本不需要任何额外的临时数组,并且只使用一个循环。
  • @cfh 这就是我的想法——矢量化对内存的影响更大。但另一方面,可以在 4 个内核上并行计算矢量化版本。并且可能存在一个收支平衡点,即 4 倍 CPU 带来的收益大于内存分配成本。你在四核上测试过吗?
  • 我认为目前这些计算不会自动分布在 Julia 的核心上。
【解决方案3】:

我无法重现你的结果。

查看这个 IJulia 笔记本:http://nbviewer.ipython.org/urls/gist.githubusercontent.com/anonymous/24c17478ae0f5562c449/raw/8d5d32c13209a6443c6d72b31e2459d70607d21b/rgb2gray.ipynb

我得到的数字是:

In [5]:

@time rgb2gray_loop(rand(50,50,3));
@time rgb2gray_vec(rand(50,50,3));

elapsed time: 7.591e-5 seconds (80344 bytes allocated)
elapsed time: 0.000108785 seconds (241192 bytes allocated)

In [6]:

@time rgb2gray_loop(rand(500,500,3));
@time rgb2gray_vec(rand(500,500,3));

elapsed time: 0.021647914 seconds (8000344 bytes allocated)
elapsed time: 0.012364489 seconds (24001192 bytes allocated)

In [7]:

@time rgb2gray_loop(rand(5000,5000,3));
@time rgb2gray_vec(rand(5000,5000,3));

elapsed time: 0.902367223 seconds (800000440 bytes allocated)
elapsed time: 1.237281103 seconds (2400001592 bytes allocated, 7.61% gc time)

正如预期的那样,循环版本对于大输入更快。另请注意矢量化版本如何分配三倍的内存。

我还想指出,gray = similar(A,size(A)[1:2]...) 语句是多余的,可以省略。 如果没有这种不必要的分配,最大问题的结果是:

@time rgb2gray_loop(rand(5000,5000,3));
@time rgb2gray_vec(rand(5000,5000,3));

elapsed time: 0.953746863 seconds (800000488 bytes allocated, 3.06% gc time)
elapsed time: 1.203013639 seconds (2200001200 bytes allocated, 7.28% gc time)

所以内存使用量下降了,但速度并没有明显提高。

【讨论】:

  • 我可以用@time 重现我的结果。我猜 Falco 是对的,结果与我机器上的某种并行化有关....
  • @reschu:这听起来不对。首先,Julia 不会自动并行化。另请注意,循环版本的给定时间比问题大小的线性增长更差:从第二个问题到第三个问题,它慢了 200 倍,尽管问题大小只大了 100 倍。那里一定发生了一些奇怪的事情。
  • 是的,你是对的。我发现它是什么。我从 Julia 开始时第一次读到的东西:Column-major order。在嵌套循环中交换行和列会产生预期的结果。
  • @reschu:这很有趣!你会添加一个答案来描述你做了什么吗?我还可能不得不撤回我关于 Julia 不会自动并行化的声明——它使用 OpenBLAS,并且在某些情况下可以利用多个内核。所以这可能是这两个因素的结合。
猜你喜欢
  • 2015-06-09
  • 1970-01-01
  • 1970-01-01
  • 2022-06-15
  • 1970-01-01
  • 2016-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多