I decided to get some quantitative results to this question -- through brute-force. I decided to write my own version of BorderDimensions
(calculates borders of a solid color around an image) using Compile
. It's non-trivial enough to actually demonstrate the point, but (I hope) small enough to post here.
Note that I'm aware that my version could be "tricked" if an image contains horizontal/vertical lines of the same color, but where each line doesn't have the same color as its adjacent line. That's not the point for this question -- it's just a proof-of-concept. Also, I think there's a bug in the "function-based" version (I ran it through some test images), but the point is that you can see a ton of TensorCopy
operations in CompilePrint
, and the results of AbsoluteTiming
speak for themselves.
The results confirmed that inlining functions yourself, manually, does actually result in performance boosts.
I used Henrik Schumacher's suggestion of using ExportString[cf, "C"]
to view the actual C-code generated, plus AbsoluteTiming
. Results: the version with function calls generated double as many lines of C-code, and took more than double time to execute, as measured by running 1000 iterations through a small test image and using AbsoluteTiming
.
First, here is the test data:
testImgRaster = Rasterize@x
testImgData =
Rasterize[x] /. HoldPattern@Image[data__] -> List[data];
testImgDataArr = testImgData[[1]] // Normal;
Next, here is the benchmarking setup:
Style["PaddingCalculatorCompiledWithFunctions", Bold, Red]
codePaddingCalculatorCompiledWithFunctions =
ExportString[PaddingCalculatorCompiledWithFunctions, "C"];
StringLength[codePaddingCalculatorCompiledWithFunctions]
Do[PaddingCalculatorCompiledWithFunctions[testImgDataArr], {i, 0,
1000}] // AbsoluteTiming
Style["PaddingCalculatorCompiledWithExpressions", Bold, Red]
codePaddingCalculatorCompiledWithExpressions =
ExportString[PaddingCalculatorCompiledWithExpressions, "C"];
StringLength[codePaddingCalculatorCompiledWithExpressions]
Do[PaddingCalculatorCompiledWithExpressions[testImgDataArr], {i, 0,
1000}] // AbsoluteTiming
Style["Comparison - BorderDimensions", Bold, Red]
Do[BorderDimensions[testImgRaster], {i, 0, 1000}] // AbsoluteTiming
Here are the benchmarking results:
PaddingCalculatorCompiledWithFunctions
15390
{0.447371,Null}
PaddingCalculatorCompiledWithExpressions
8898
{0.196994,Null}
Comparison - BorderDimensions
{0.180333,Null}
Finally, if you've read to here and want to see the actual code, here it is...
Note: I normally go way out-of-my-way to make small, concise, purpose-built functions. But in addition to the performance benefits, I think I like the "look and feel" of the expression-based version better. The style takes some getting-used-to, though.
Function-based version:
PaddingCalculatorCompiledWithFunctions =
Module[{PaddingCalculatorGenerator, PaddingCalculatorParams, numRows,
numCols, PaddingCalculator, PaddingCalculatorInner},
PaddingCalculatorGenerator[primaryRange_, secondaryRange_,
comparePart1_, comparePart2_] =
Function[imageDataArgToCalculator,
Hold@Do[
Function[innerResult,
If[innerResult == -1,
Return@Abs@(primaryRange[[2]] - primaryRange[[1]])]
]@
Do[
If[
comparePart1@imageDataArgToCalculator ==
comparePart2@imageDataArgToCalculator,(*Null*)-1, Return@-1],
secondaryRange
],
primaryRange
]
];
PaddingCalculatorParams[numRows_, numCols_] =
{
{{rowIdx, 1, numRows, +1}, {colIdx, 1, numCols},
Part[#, rowIdx, colIdx] &, Part[#, rowIdx, 1] &},
{{rowIdx, numRows, 1, -1}, {colIdx, 1, numCols},
Part[#, rowIdx, colIdx] &, Part[#, rowIdx, 1] &},
{{colIdx, 1, numCols, +1}, {rowIdx, 1, numRows},
Part[#, rowIdx, colIdx] &, Part[#, 1, colIdx] &},
{{colIdx, numCols, 1, -1}, {rowIdx, 1, numRows},
Part[#, rowIdx, colIdx] &, Part[#, 1, colIdx] &}
};
PaddingCalculator =
ReleaseHold@
Function[{imageDataArgToCalculators, numRows, numCols},
imageDataArgToCalculators //
Apply[PaddingCalculatorGenerator] /@
PaddingCalculatorParams[numRows, numCols] // Through //
Evaluate
];
PaddingCalculatorInner =
With[{PaddingCalculator = PaddingCalculator},
ReleaseHold@Function[imageDataArgToMain,
Module[{numRows, numCols},
numRows = Hold@Length@imageDataArgToMain;
numCols = Hold@Length@First@imageDataArgToMain;
Hold@PaddingCalculator[imageDataArgToMain, numRows, numCols]
]
]
];
With[{PaddingCalculatorInner = PaddingCalculatorInner},
Compile[{{imageDataArgToCompile, _Integer, 3}},
PaddingCalculatorInner[imageDataArgToCompile]]
]
]
Expression-based version:
ReleaseHoldUnevaluated[expr_] :=
ReplaceRepeated[HoldComplete[Unevaluated[expr]], Hold[x__] -> x] //
ReleaseHold
CompileSustitutable[vars_, body_] :=
Hold[vars, body] /. Hold -> Compile
Module[{PaddingCalculatorGenerator, PaddingCalculatorParams,
PaddingCalculator, PaddingCalculatorInner},
PaddingCalculatorGenerator[primaryRange_, secondaryRange_,
comparePart1_, comparePart2_] :=
With[{primaryIndex = primaryRange[[1]],
primaryStart = primaryRange[[2]]},
Hold@Do[
Function[innerResult,
If[innerResult == -1,
Return@Abs@(primaryStart - primaryIndex)] ]@
Do[
If[comparePart1 == comparePart2, Null, Return@-1],
secondaryRange
],
primaryRange
]
];
ImagePart[row_, col_] = Hold@Part[imageData, row, col];
PaddingCalculatorParams =
{
{{rowIdx, 1, numRows, +1}, {colIdx, 1, numCols},
ImagePart[rowIdx, colIdx], ImagePart[rowIdx, 1]},
{{rowIdx, numRows, 1, -1}, {colIdx, 1, numCols},
ImagePart[rowIdx, colIdx], ImagePart[rowIdx, 1]},
{{colIdx, 1, numCols, +1}, {rowIdx, 1, numRows},
ImagePart[rowIdx, colIdx], ImagePart[1, colIdx]},
{{colIdx, numCols, 1, -1}, {rowIdx, 1, numRows},
ImagePart[rowIdx, colIdx], ImagePart[1, colIdx]}
};
PaddingCalculator =
Hold[Module]
[{numRows, numCols},
Hold[CompoundExpression][
Hold[numRows = Length@imageData],
Hold[numCols = Length@First@imageData],
PaddingCalculatorGenerator @@@ PaddingCalculatorParams
]
];
PaddingCalculatorCompiledWithExpressions =
CompileSustitutable[{{imageData, _Integer, 3}},
ReleaseHoldUnevaluated[PaddingCalculator]]
]
R1 = I0
is a type cast from 64 bit integer to 64 bit float (double). Yes, it does matter, though often only a bit. Anyways, you can get with from it by applyN
to the expression before handing it over toCompile
. $\endgroup$R1 = R0
is weird though. But the compiler will strip this out very likely. $\endgroup$ExportString[cf, "C"]
. I'll post a response including the results of using this on a test function in a moment. Regarding the use ofN[...]
, can you clarify how that would avoid the extra function calls / argument copying, using the example I posted forFunction[x, Evaluate@Through@{# &, # + 1 &, #^2 &}@x]
? It seems to me that the argument wouldn't be known until you're running the compiled function, so I don't see how usingN[...]
at that point would reduce the overhead. But I'm tired ;-) $\endgroup$