I have a performance issue with FFI calls which turns out to be my fault: I am building the call on every call. However, the current interface provided by Foreign does not make the fix easy in this particular case because buildCall<N> functions require the symbol to build the call, which isn't actually necessary.
The application I am building uses ML-NLFFI to generate the ML code to call C functions. For now I have adapted the MLton version of ML-NLFFI to generate code for Poly/ML instead - a fairly minor tweak. ML-NLFFI generates a 'Callop' structure for each combination of argument and return value types. Initially, I didn't use the structure Foreign but instead used an alternative structure PolyMLFFI that implements the signature POLYML_F_F_I, giving a Callop structure as follows:
structure Callop_3 = struct
local
open PolyMLFFI
in
val callop = call (cPointer &&> cPointer &&> cPointer --> cPointer)
end
end
This function can be called as Callop_3.callop sym (arg1 & arg2 & arg3). Notably, the symbol is supplied at the time of the call.
PolyMLFFI avoids the buildCall<N> family of functions by using nested tuples, introducing infix operators to make such expressions more readable. (Aside: It would be nice if ML had a way to construct functions on tuples by composition.) This seems to work well enough but I had concerns about inlining of the FFI call code so wanted to compare performance with using Foreign instead. The equivalent Callop structure to above is:
structure Callop_3 = struct
local
open Foreign
in
fun callop sym
= buildCall3 (sym, (cPointer, cPointer, cPointer), cPointer)
end
end
Of course, buildCall3 is not evaluated until Callop_3.callop is applied to sym which is done only at the time of a call, hence my performance issue. I think I could cache the built call, for example, by evaluating
val callSym = Lazy.lazy (fn () => Callop_3.callop sym)
once and calling as callSym () (arg1, arg2, arg3), using Lazy.lazy to implement this common pattern.
Still, it would be good if Foreign supported this use-case directly. I think Foreign is easily changed by moving the symbol argument out of the callF argument of buildCall:
fun buildCall(argConv, resConv, callF) =
let
val { ctype = resType, load = resLoad, ...} = resConv
val { store=storeArgs, ctype={size=argSize, ...}, updateML=updateArgs, ...} = argConv
val resultOffset = alignUp(argSize, #align resType)
val argResSpace = resultOffset + #size resType
in
fn fnAddr => fn mlArgs =>
alloca(argResSpace,
fn rMem =>
let
val freeArgs = storeArgs(rMem, mlArgs)
val resultAddr = rMem++resultOffset
in
let
val () = callF fnAddr (rMem, resultAddr)
val result = resLoad resultAddr
in
updateArgs(rMem, mlArgs);
freeArgs();
result
end handle exn => (freeArgs(); raise exn)
end)
end
and then e.g.
fun buildCall3withAbi (abi: abi,
(arg1Conv: 'a conversion, arg2Conv: 'b conversion, arg3Conv: 'c conversion), resConv: 'd conversion): symbol -> 'a * 'b *'c -> 'd =
let
val callF = callwithAbi abi [#ctype (breakConversion arg1Conv), #ctype (breakConversion arg2Conv), #ctype (breakConversion arg3Conv)] (#ctype (breakConversion resConv))
val argConv = cStruct3(arg1Conv, arg2Conv, arg3Conv)
in
buildCall(argConv, resConv, callF)
end
Unfortunately this changes the type of the buildCall<N> functions so breaks existing code.
I have a performance issue with FFI calls which turns out to be my fault: I am building the call on every call. However, the current interface provided by Foreign does not make the fix easy in this particular case because buildCall<N> functions require the symbol to build the call, which isn't actually necessary.
The application I am building uses ML-NLFFI to generate the ML code to call C functions. For now I have adapted the MLton version of ML-NLFFI to generate code for Poly/ML instead - a fairly minor tweak. ML-NLFFI generates a 'Callop' structure for each combination of argument and return value types. Initially, I didn't use the structure Foreign but instead used an alternative structure PolyMLFFI that implements the signature POLYML_F_F_I, giving a Callop structure as follows:
structure Callop_3 = struct local open PolyMLFFI in val callop = call (cPointer &&> cPointer &&> cPointer --> cPointer) end endThis function can be called as Callop_3.callop sym (arg1 & arg2 & arg3). Notably, the symbol is supplied at the time of the call.
PolyMLFFI avoids the buildCall<N> family of functions by using nested tuples, introducing infix operators to make such expressions more readable. (Aside: It would be nice if ML had a way to construct functions on tuples by composition.) This seems to work well enough but I had concerns about inlining of the FFI call code so wanted to compare performance with using Foreign instead. The equivalent Callop structure to above is:
structure Callop_3 = struct local open Foreign in fun callop sym = buildCall3 (sym, (cPointer, cPointer, cPointer), cPointer) end endOf course, buildCall3 is not evaluated until Callop_3.callop is applied to sym which is done only at the time of a call, hence my performance issue. I think I could cache the built call, for example, by evaluating
once and calling as callSym () (arg1, arg2, arg3), using Lazy.lazy to implement this common pattern.
Still, it would be good if Foreign supported this use-case directly. I think Foreign is easily changed by moving the symbol argument out of the callF argument of buildCall:
fun buildCall(argConv, resConv, callF) = let val { ctype = resType, load = resLoad, ...} = resConv val { store=storeArgs, ctype={size=argSize, ...}, updateML=updateArgs, ...} = argConv val resultOffset = alignUp(argSize, #align resType) val argResSpace = resultOffset + #size resType in fn fnAddr => fn mlArgs => alloca(argResSpace, fn rMem => let val freeArgs = storeArgs(rMem, mlArgs) val resultAddr = rMem++resultOffset in let val () = callF fnAddr (rMem, resultAddr) val result = resLoad resultAddr in updateArgs(rMem, mlArgs); freeArgs(); result end handle exn => (freeArgs(); raise exn) end) endand then e.g.
fun buildCall3withAbi (abi: abi, (arg1Conv: 'a conversion, arg2Conv: 'b conversion, arg3Conv: 'c conversion), resConv: 'd conversion): symbol -> 'a * 'b *'c -> 'd = let val callF = callwithAbi abi [#ctype (breakConversion arg1Conv), #ctype (breakConversion arg2Conv), #ctype (breakConversion arg3Conv)] (#ctype (breakConversion resConv)) val argConv = cStruct3(arg1Conv, arg2Conv, arg3Conv) in buildCall(argConv, resConv, callF) endUnfortunately this changes the type of the buildCall<N> functions so breaks existing code.