基准测试 Data.Vector 时出现意外低的时间

4
我正在测试Haskell的数组库(array和vector包)以找到存储大量数据的最佳方法。我使用criterion作为基准测试工具。
简而言之,我的代码只是分配了一个向量,然后用简单的结构体填充它(分别为100万、1000万和1亿个元素)。当我将Haskell的基准测试时间与我用C编写的简单参考实现进行比较时,Haskell要快几倍,我觉得这很可疑:C代码是一个简单的循环,用结构体填充数组。
问题是:Haskell的vector库是否可能在性能方面击败C?还是说我的基准测试有缺陷/某些内容未被评估/存在一些“陷阱”?
另一个问题是如何确保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'

基准测试是衡量标准的一项:

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

Gist 包含其他基准测试,尝试以不同的方式强制评估。
可以在这里(Gist)找到执行更多或更少相同操作的参考C代码。主要逻辑如下:
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 Gist,我会尝试以不同的方式强制进行评估)。我做错了什么吗?如果没有错的话-如何让vector在C中击败一个简单的for循环(GCC进一步展开)?

请原谅我这个非常冗长的问题,我想尽可能提供整个背景 ;)


3
它很不可能在一纳秒内实际填充向量。看起来很可能以某种方式将整个程序优化为未使用,从而消失不见。 - dfeuer
1
请在您的问题中包含实际的C代码,而不仅仅是链接。如果没有要比较的代码,您的问题就毫无意义了。 - Cubic
@dfeuer 对的,我稍微简化了代码以提取最小的示例。在我的原始代码中(结构体更复杂,但仍然不算不寻常),计时更合理。这就是为什么我很好奇Haskell代码/基准测试有什么问题。 - piotrMocz
1
不是不可能,GHC 可能会推断出矢量操作可以进行向量化或并行化,而 GCC 无法对 C 代码进行相同的操作。C 的一个方面已经显得有点过时了,那就是你必须将所有的矢量操作写成连续的循环,这些循环可能会以任意的方式干扰任何元素或者循环索引本身,特别是在涉及指针别名时更是如此。现在,你真正想让编译器做的是找出:“哦,你只是对每个元素执行相同的操作 i 或 a[i]”,并将其优化为 forall语义。 - Davislor
1
但你怀疑是正确的。你可以尝试使用-S编译并检查汇编代码,但也许C版本会受益于一个#pragma omp指令。 - Davislor
显示剩余12条评论
1个回答

1

虽然我不信任基准测试代码,但我也无法复现这个问题。我修改了Haskell的gist(只删除了后两个基准测试),并且修改了C语言的基准测试(使其执行操作1000次,然后将时间除以1000)。

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

  1. 您正在使用具有隐含合同的unsafe*调用,而您违反了这些合同。
  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

即,在1000万个元素中,C语言运行时间为11毫秒。
和。
% 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)

例如,在10^7个结果下,Haskell运行时间为23毫秒。
这是在一台配备GHC 8.2的较新MacBook上进行的测试。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接