"bracket (mallocBytes n) free" 和 "allocaBytes" 有什么区别?

What is the difference between "bracket (mallocBytes n) free" and "allocaBytes"?

如果需要背景,请参阅here. In short, the question is: “What's actual difference between bracket (mallocBytes n) free and allocaBytes from Foreign.Marshall.Alloc”。

通常在 C 中,alloca 在堆栈上分配,malloc 在堆上分配。我不确定 Haskell 中发生了什么,但除了速度之外,我不希望上述方程式之间存在差异。但是,如果您单击背景 link,您就会知道编译后的代码 bracket (mallocBytes n) free 会导致“双重释放或损坏”,而 allocaBytes 工作正常(在 GHCi 中该问题不可见所有,在这两种情况下一切正常。

到目前为止,我已经花了两天时间进行痛苦的调试,而且我非常有信心 bracket (mallocBytes n) free 不知何故不稳定,其余代码是可靠的。我想知道 bracket (mallocBytes n) free.

是怎么回事

bracket (mallocBytes size) free 将使用 C 的 mallocfree,而 allocaBytes size 将使用由 GHC 垃圾回收管理的内存。这本身已经是一个巨大的差异,因为 allocaBytesPtr 可能被未使用(但已分配)的内存包围:

import Control.Exception
import Control.Monad (forM_)
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable

-- Write a value at an invalid pointer location
hammer :: Ptr Int -> IO ()
hammer ptr = pokeElemOff ptr (-1) 0 >> putStrLn "hammered"

main :: IO ()
main = do
  putStrLn "Hammer time! Alloca!"
  forM_ [1..10] $ \n ->
    print n >> allocaBytes 10 hammer

  putStrLn "Hammer time! Bracket"
  forM_ [1..10] $ \n ->
    print n >> bracket (mallocBytes 10) free hammer

结果:

Hammer time! Alloca!
1
hammered
2
hammered
3
hammered
4
hammered
5
hammered
6
hammered
7
hammered
8
hammered
9
hammered
10
hammered
Hammer time! Bracket
1
hammered
<program crashes>

如您所见,尽管我们使用了 arr[-1] = 0,但 allocaBytes 愉快地忽略了该错误。但是,如果您写到位置 -1free 会(经常)当着您的面爆炸。如果另一个分配的内存区域发生内存损坏,它也会当面爆炸*。

此外,对于 allocaBytes,指针很可能指向已分配内存的某处,而不是内存的开头,例如

nursery = malloc(NURSERY_SIZE);

// ...

pointer_for_user = nursery + 180;

// pointer_for_user[-1] = 0 is not as 
// much as a problem, since it doesn't yield undefined behaviour

这是什么意思?好吧,allocaBytes 不太可能在您面前爆炸,但代价是您不会注意到您的 C 代码变体是否会导致内存损坏。更糟糕的是,一旦您在 allocaBytes 返回的范围之外写入,您可能会悄悄破坏其他 Haskell 值。

但是,我们在这里讨论的是未定义的行为。上面的代码可能会或可能不会在您的系统上崩溃。它也可能在 allocaBytes 部分崩溃。

如果我是你,我会trace the malloc and free calls


* 我曾经在我的程序中间遇到一个 "double use of free" 错误。调试了所有内容,重写了大部分 "bad" 例程。不幸的是,错误在调试版本中消失了,但在发布版本中再次出现。原来在main的前十行,不小心把b[i - 1]写成了i = 0

我能够重现该问题,并且可以确认存在大量缓冲区溢出。如果您使用以下分配器(请原谅快速和肮脏的代码),它在缓冲区后添加一个页面的价值 0xa5s 并在它被修改时将其转储出来,您可以在几个测试中看到数百字节的溢出:

withBuffer :: Int -> (Ptr a -> IO b) -> IO b
withBuffer n = bracket begin end
  where begin = do
          a <- mallocBytes (n + 4096)
          mapM_ (\i -> pokeByteOff (a `plusPtr` n) i (0xa5 :: Word8)) [0..4095]
          return a
        end = \a -> do
          page <- mapM (\i -> peekByteOff (a `plusPtr` n) i) [0..4095]
          when (any (/= (0xa5 :: Word8)) page) $ do
            putStrLn $ unlines $ map (hexline page) [0,16..4095]
            error "corruption detected"
          free a
        hexline bytes off = unwords . map hex . take 16 . drop off $ bytes
        hex = printf "%02x"