我用Haskell创建了一个DLL。它导出两个函数:
readXLSX
:将xlsx文件名,工作表名称作为参数,并在R列表中返回工作表的内容
readXLSXbig
:没有参数,这与readXLSX
的功能相同,但它始终读取名为Sheet 1
首先,让我们在一个小文件上尝试readXLSX
:
> library(readxl)
> library(microbenchmark)
> microbenchmark(
+ readxl = read_xlsx("example.xlsx", "Sheet1", col_types="list", col_names=FALSE),
+ haskell = .Call("readXLSX", "example.xlsx", "Sheet1"),
+ times=2
+ )
Unit: milliseconds
expr min lq mean median uq max neval cld
readxl 9.294365 9.294365 9.660375 9.660375 10.026385 10.026385 2 b
haskell 2.164942 2.164942 2.593681 2.593681 3.022419 3.022419 2 a
嗯,它比readxl
更快(不足为奇,因为readxl
提供了更多功能,并测试了文件是否存在等。)
现在,让我们尝试更大的文件big.xlsx
:
> microbenchmark(
+ readxl = read_xlsx("big.xlsx", "Sheet 1", col_types="list", col_names=FALSE),
+ H1 = .Call("readXLSXbig"),
+ H2 = .Call("readXLSX", "big.xlsx", "Sheet 1"),
+ times=2
+ )
Unit: milliseconds
expr min lq mean median uq max neval cld
readxl 143.74213 143.74213 144.05025 144.05025 144.3584 144.3584 2 a
H1 33.38596 33.38596 68.60333 68.60333 103.8207 103.8207 2 a
H2 8845.17038 8845.17038 9149.19542 9149.19542 9453.2205 9453.2205 2 b
事实上,这个基准测试的结果变化很大(我不知道为什么),但我总是观察到相同的行为:readXLSXbig
比readxl
和{{1}更快很慢。
让我回想一下,readXLSX
与readXLSXbig
完全相同;唯一的区别是readXLSX
已经提供了文件名big.xlsx
和工作表名称Sheet 1
:
readXLSXbig
为什么会出现这样的差异? readXLSXbig :: SEXP V 'R.Vector
readXLSXbig = unsafePerformIO $ do
fcellmap <- xlsxSheetToFormattedCellMap "big.xlsx" "Sheet 1"
formattedCellMapToRList fcellmap fcellToCellValue 0
readXLSX :: SEXP s 'R.String -> SEXP s 'R.String -> SEXP V 'R.Vector
readXLSX file sheet = unsafePerformIO $ do
fcellmap <- xlsxSheetToFormattedCellMap (fromSEXP file) (fromSEXP sheet)
formattedCellMapToRList fcellmap fcellToCellValue 0
在大文件上需要大约9秒,而readXLSX
只需要大约70毫秒。在小文件readXLSXbig
上,我们看到example.xlsx
需要<3毫秒,因此没有浪费9秒来读取文件名和工作表名称。
答案 0 :(得分:3)
TL; DR:unsafe
内容为unsafe
。
此代码
readXLSXbig :: SEXP V 'R.Vector
readXLSXbig = unsafePerformIO $ do ...
声明一个常量表达式,它很可能在整个程序中只被评估一次。第一个之后的每个后续调用都可以重新执行do ...
操作,或者返回先前计算的值而不重新执行任何操作。
基本上,GHC正在缓存整个电子表格。
unsafePerformIO action
告诉编译器我们不关心运行action
的时间或运行次数,因为IO action
永远不会执行重要的一面效果,并将始终返回相同的结果。我们告诉编译器&#34;我知道这可能不安全,但无论如何都要这样做 - 我将承担责任&#34;。
作为一个丑陋,丑陋的修复,你可以添加一个虚拟参数,并祈祷GHC不会缓存以前的结果。
实际的修复方法是删除unsafe
内容,然后返回正确的IO (...)
类型。通过这种方式,我们不会谎言到编译器,这将做正确的事情。 FFI机器仍然可以从R中调用。