在haskell在不再引用它们之后立即完成外部指针

时间:2014-07-27 12:05:41

标签: haskell

有没有办法确保某些特定类型的对象(基本上ForeignPtr)被非常积极地垃圾收集?我有这样一种简单的类型:

data SomePtr = SomePtr { ptr :: ForeignPtr CUChar, size :: CSize }
alloc :: CSize -> IO SomePtr
free :: SomePtr -> IO ()
free = finalizeForeignPtr . ptr

我认为标准理论是引用计数(我自己也会这样做,比方说,C ++)比ghc使用的GC慢,这就是为什么它没有使用它。但对我来说问题是,当使用外部分配的对象(如GPU内存)时,最终被释放的承诺是不够的。内存非常稀缺,据我所知,ForeignPtr终结器实际上并没有被调用。我希望尽快释放内存,所以我最终自己打电话给finalizeForeignPtr

是否有某种方法可以告诉ghc在破坏某些特定类型的对象时非常积极?

或者我的方式错了?

以下示例代码用于说明我的意思:

的Haskell

{-# LANGUAGE RecordWildCards #-}
import Foreign.ForeignPtr.Safe
import Foreign.Ptr
import Foreign.Marshal.Alloc
import Foreign.Storable
import Control.Monad
import Foreign.C.Types
import Text.Printf

data FPtr = FPtr { fptr :: ForeignPtr CUChar, size :: CSize }

foreign import ccall "falloc" falloc :: CSize -> Ptr (Ptr CUChar) -> IO CInt
foreign import ccall "&ffree" ffree :: FunPtr (Ptr CUChar -> IO ())

newFPtr :: CSize -> IO FPtr
newFPtr size =
  do alloca $ \ptr -> do
       result <- falloc size ptr
       printf "Result: %d\n" (fromIntegral result :: Int)
       fptr <- newForeignPtr ffree =<< peek ptr
       return FPtr{..}

freeFPtr :: FPtr -> IO ()
freeFPtr = finalizeForeignPtr . fptr

main :: IO ()
main = forM_ [1 .. 5] $ const work
  where
  work = do x <- newFPtr 1024
            -- freeFPtr x
            return ()

C ++

#include <cstdio>
using namespace std;

extern "C" {
int falloc(size_t size, unsigned char** ptr);
void ffree(unsigned char* ptr);
}

int some_counter = 0;

int falloc(size_t size, unsigned char** ptr) {
  some_counter++;
  printf("falloc(%lu, %#lx, %#lx); %d\n",
    size, (unsigned long)ptr, (unsigned long)*ptr, some_counter);
  *ptr = new unsigned char[size];
  return 0;
}

void ffree(unsigned char* ptr) {
  printf("ffree(%#lx)\n", (unsigned long)ptr);
  delete[] ptr;
}

输出

falloc(1024, 0x100606010, 0); 1
Result: 0
falloc(1024, 0x100606028, 0); 2
Result: 0
falloc(1024, 0x100606040, 0); 3
Result: 0
falloc(1024, 0x100606058, 0); 4
Result: 0
falloc(1024, 0x100606070, 0); 5
Result: 0
ffree(0x101026400)
ffree(0x101027800)
ffree(0x101027c00)
ffree(0x101028000)
ffree(0x101028400)

期望的输出

falloc(1024, 0x100606010, 0); 1
Result: 0
ffree(0x101026400)
falloc(1024, 0x100606028, 0); 2
Result: 0
ffree(0x100802200)
falloc(1024, 0x100606040, 0); 3
Result: 0
ffree(0x100802200)
falloc(1024, 0x100606058, 0); 4
Result: 0
ffree(0x100802200)
falloc(1024, 0x100606070, 0); 5
Result: 0
ffree(0x100802200)

3 个答案:

答案 0 :(得分:2)

你正在以错误的方式考虑这个问题 for Haskell。

在C ++中,RAII用于确保及时释放资源。由于C ++缺少finally构造,因此没有其他方法可以确保在存在异常的情况下释放资源。此外,由于C ++缺少垃圾收集器,引用计数和RAII是当天的顺序。

然而,在Haskell(以及其他垃圾收集语言)中,情况有所不同。一个人不依赖于即时运行的终结器。实际上,根本不应该依赖于终结器运行,因为如果可用内存量足够高,它们可能会被延迟一段任意时间 - 并且如果永远不会被执行程序在终结器(甚至垃圾收集器)有机会运行之前终止,因为对象无法访问。

相反,使用显式资源释放。这似乎很糟糕,但并非如此。出于记忆安全的原因,人们应该把对象放在&#34; zombie&#34;状态,以便任何进一步尝试使用该对象抛出异常(因为它们是错误)。

或者,如果资源是在进程退出时自动释放的,那么可以依赖终结器 - 但请注意,它们可能不会被及时调用(如您所述),因此显式performGC如果资源耗尽,可能需要。我怀疑不知道真正稀缺资源的生命何时结束(至少保守)可能是代码味道,即使在C ++中 - 这意味着消耗的资源量没有上限。

答案 1 :(得分:2)

如果我们想要独立于GHC的垃圾收集,我们需要引入某种决定论,因此需要明确的解除分配。分配通常是类型为IO a的类型,以及类型a -> IO ()的相应解除分配(就像您的示例一样)。

现在,如果我们有以下功能怎么办?

allocate  :: IO a -> (a -> IO ()) -> Alloc a
runAlloc  :: Alloc a -> IO a

autoAllocate应该同时进行分配和释放,并在新(表面)Alloc monad中提供分配结果,runAlloc运行所有操作和解除分配。你的例子不会改变那么多,除了结尾:

allocateFPtr size = autoAllocate (newFPtr size) freeFPtr

main :: IO ()
main = forM_ [1 .. 5] $ runAlloc . const work
  where
  work = do x <- allocateFPtr 1024
            return ()

现在,allocaterunAllocAlloc已作为resourcetallocaterunResourceT存在于ResourceT中,并且实际代码如下所示:

allocateFPtr size = fmap snd $ allocate (newFPtr size) freeFPtr

main :: IO ()
main = forM_ [1 .. 5] $ runResourceT . const work
  where
  work = do x <- allocateFPtr 1024
            return ()

结果:

falloc(1024, 0x1e04014, 0); 1
Result: 0
ffree(0x6abc60)
falloc(1024, 0x1e04020, 0); 2
Result: 0
ffree(0x6abc60)
falloc(1024, 0x1e0402c, 0); 3
Result: 0
ffree(0x6abc60)
falloc(1024, 0x1e04038, 0); 4
Result: 0
ffree(0x6abc60)
falloc(1024, 0x1e04044, 0); 5
Result: 0
ffree(0x6abc60)

但你说你的一些指针实际上应该活得更久。这也不是问题,因为allocate实际上返回m (ReleaseKey, a),而ReleaseKey可用于在runResourceT之前释放内存(使用{{3} }}或删除自动释放机制(使用release,它返回释放操作)。

总而言之,我猜你的情景可以通过ResourceT很好地处理。毕竟,它的概要是&#34;确定性分配和释放稀缺资源&#34;。

答案 2 :(得分:2)

在非常有限的情况下,您只关心释放一些可以在haskell堆上存活的内存,可以使用特殊的边缘情况。

mallocForeignPtr将内存分配为haskell堆上的固定可变字节数组,因此当ForeignPtr(和可变字节数组)得到GC时,内存会自动回收到没有终结器调用。

这比添加手动挂钩来调用与系统free相对应的某些malloc要便宜得多,但仅限于您可以忍受限制的有限情况。

但是,如果您依赖于释放其他资源(例如通过文件句柄对象,或GPU或其他内容中的内存或资源ID),您仍然需要进行管理。

一般情况下,不要依赖GC为您释放宝贵的外部资源,除非作为一种道歉&#34;在例如异常等期间通过泄露的东西。您通常的控制流程仍应释放您使用的外部资源。