基准测试时出人意料的低时间 Data.Vector

Surprisingly low times when benchmarking Data.Vector

我正在对 Haskell 的数组库(arrayvector 包)进行基准测试,以便为我的用例提出存储大数据的最佳方法。我正在使用 criterion 作为基准测试工具。

长话短说: 我的代码只是分配一个向量,然后用简单的结构(分别为 1M、10M 和 100M 元素)填充它。当我将 Haskell 基准测试时间与我用 C 编写的简单参考实现进行比较时,Haskell 快了几倍,我发现它很可疑:C 代码是一个简单的循环,用于填充数组中的结构.

问题: Haskell 的 vector 库是否有可能在性能方面击败 C?或者这是否意味着我的基准是 flawed/something 实际上不是 evaluated/there 的一些 'gotcha'?

另一个问题如何确保 Haskell 向量被实际计算?

更长的解释:手头的任务是用大量结构填充向量。他们有 Storable 个实例,使用的向量是 Data.Vector.Storable.

数据类型如下:

data Foo = Foo Int Int deriving (Show, Eq, Generic, NFData)

Storable 个实例如下所示:

chunkSize :: Int
chunkSize = sizeOf (undefined :: Int)
{-# INLINE chunkSize #-}

instance Storable Foo where
    sizeOf    _ = 2 * chunkSize ; {-# INLINE sizeOf    #-}
    alignment _ = chunkSize     ; {-# INLINE alignment #-}
    peek ptr = Foo
        <$> peekByteOff ptr 0
        <*> peekByteOff ptr chunkSize
    {-# INLINE peek #-}
    poke ptr (Foo a b) = do
        pokeByteOff ptr 0 a
        pokeByteOff ptr chunkSize b
    {-# INLINE poke #-}

序列化本身似乎工作正常。然后分配向量:

mkFooVec :: Int -> IO (Vector Foo)
mkFooVec !i = unsafeFreeze =<< new (i + 1)

并填充结构:

populateFooVec :: Int -> Vector Foo -> IO (Vector Foo)
populateFooVec !i !v = do
    v' <- unsafeThaw v
    let go 0 = return ()
        go j = unsafeWrite v' j (Foo j $ j + 1) >> go (j - 1)
    go i
    unsafeFreeze v'

Benchmark是标准准则之一:

    defaultMain [
      bgroup "Storable vector (mutable)"
        $ (\(i :: Int) -> env (mkFooVec (10 ^ i))
        $ \v -> bench ("10e" <> show i)
        $ nfIO (populateFooVec (10 ^ i) v))  <$> [6..8]
    ]

gist 包含其他基准,试图以不同的方式强制评估。

可以找到大致相同的参考 C 代码 here (gist)。主要逻辑如下:

Foo *allocFoos(long n) {
    return (Foo *) malloc(n * sizeof(Foo));
}

// populate the array with structs:
void createFoos(Foo *v, long n) {
    for (long i = 0; i < n; ++i) {
        v[i].name = i;
        v[i].id = i + 1;
    }
}

以及用于运行它的命令:gcc -O2 -o bench benchmark.c && ./bench

现在,当我 运行 基准测试时,C 代码大约需要 50 毫秒,而 Criterion 报告结果大约需要 800 皮秒 (!)。这让我想知道:也许我对结果的解释有误?也许矢量实际上并未被评估(尽管如果您查看 Haskell 要点,我会尝试以不同的方式强制评估)。我究竟做错了什么?如果没有 - vector 如何击败 C 中的简单 for 循环(GCC 进一步展开,顺便说一句)?

请原谅我的问题太长了,我试图给出整个上下文 ;)

虽然我不信任基准测试代码,但我也无法重现该问题。我修改了 Haskell 要点(只是删除了第二个两个基准)和 C 基准(让它执行操作 1000 次然后将时间除以 1000)。

编辑:我不信任代码,因为:

  1. 您正在使用不安全*调用,这些调用具有您违反的隐式约定。
  2. 该代码甚至无法编译 - 您有错字和缺少语言扩展。这通常是其他恶作剧的迹象。

我的结果

结果如何?没错,这里没有异常。

% gcc bench.c -O3 && ./a.out
Starting the benchmark
[[ Malloced-array-[10000000] ]]Time taken: 11.904249 ms (cpu) 11.904249 ms (wall)
Done
./a.out  11.78s user 0.14s system 98% cpu 12.131 total

11ms 对于 C 在 10^7 个元素。

% ghc -O2 bench.hs && ./bench
benchmarking Storable vector (FAKE mutable)/10e6
time                 2.362 ms   (2.236 ms .. 2.561 ms)
                     0.953 R²   (0.909 R² .. 0.989 R²)
mean                 2.344 ms   (2.268 ms .. 2.482 ms)
std dev              305.0 μs   (169.1 μs .. 477.1 μs)
variance introduced by outliers: 79% (severely inflated)

benchmarking Storable vector (FAKE mutable)/10e7
time                 23.37 ms   (22.13 ms .. 24.73 ms)
                     0.989 R²   (0.979 R² .. 0.996 R²)
mean                 23.19 ms   (22.63 ms .. 23.76 ms)
std dev              1.287 ms   (1.015 ms .. 1.713 ms)
variance introduced by outliers: 19% (moderately inflated)

benchmarking Storable vector (FAKE mutable)/10e8
time                 232.2 ms   (215.1 ms .. 247.3 ms)
                     0.994 R²   (0.974 R² .. 1.000 R²)
mean                 223.5 ms   (215.9 ms .. 231.5 ms)
std dev              10.41 ms   (7.887 ms .. 13.06 ms)
variance introduced by outliers: 14% (moderately inflated)

23ms 对于 Haskell 在 10^7 结果。

这是在带有 GHC 8.2 的较新的 macbook 上。