Requesting Help with Optimizing Code
Max Haughton
maxhaton at gmail.com
Thu Apr 8 03:27:12 UTC 2021
On Thursday, 8 April 2021 at 01:24:23 UTC, Kyle Ingraham wrote:
> Hello all. I have been working on learning computational
> photography and have been using D to do that. I recently got
> some code running that performs [chromatic
> adaptation](https://en.wikipedia.org/wiki/Chromatic_adaptation)
> (white balancing). The output is still not ideal (image is
> overexposed) but it does correct color casts. The issue I have
> is with performance. With a few optimizations found with
> profiling I have been able to drop processing time from ~10.8
> to ~6.2 seconds for a 16 megapixel test image. That still feels
> like too long however. Image editing programs are usually much
> faster.
>
> The optimizations that I've implemented:
> * Remove `immutable` from constants. The type mismatch between
> constants (`immutable(double)`) and pixel values (`double`)
> caused time-consuming checks for compatible types in mir
> operations and triggered run-time type conversions and memory
> allocations (sorry if I butchered this description).
> * Use `mir.math.common.pow` in place of `std.math.pow`.
> * Use `@optmath` for linearization functions
> (https://github.com/kyleingraham/photog/blob/up-chromadapt-perf/source/photog/color.d#L192 and https://github.com/kyleingraham/photog/blob/up-chromadapt-perf/source/photog/color.d#L318).
>
> Is there anything else I can do to improve performance?
>
> I tested the code under the following conditions:
> * Compiled with `dub build --build=release --compiler=ldmd2`
> * dub v1.23.0, ldc v1.24.0
> * Intel Xeon W-2170B 2.5GHz (4.3GHz turbo)
> * [Test
> image](https://user-images.githubusercontent.com/25495787/113943277-52054180-97d0-11eb-82be-934cf3d22112.jpg)
> * Test code:
> ```d
> #!/usr/bin/env dub
> /+ dub.sdl:
> name "photog-test"
> dependency "photog" version="~>0.1.1-alpha"
> dependency "jpeg-turbod" version="~>0.2.0"
> +/
>
> import std.datetime.stopwatch : AutoStart, StopWatch;
> import std.file : read, write;
> import std.stdio : writeln, writefln;
>
> import jpeg_turbod;
> import mir.ndslice : reshape, sliced;
>
> import photog.color : chromAdapt, Illuminant, rgb2Xyz;
> import photog.utils : imageMean, toFloating, toUnsigned;
>
> void main()
> {
> const auto jpegFile = "image-in.jpg";
> auto jpegInput = cast(ubyte[]) jpegFile.read;
>
> auto dc = new Decompressor();
> ubyte[] pixels;
> int width, height;
> bool decompressed = dc.decompress(jpegInput, pixels, width,
> height);
>
> if (!decompressed)
> {
> dc.errorInfo.writeln;
> return;
> }
>
> auto image = pixels.sliced(height, width, 3).toFloating;
>
> int err;
> double[] srcIlluminant = image
> .imageMean
> .reshape([1, 1, 3], err)
> .rgb2Xyz
> .field;
> assert(err == 0);
>
> auto sw = StopWatch(AutoStart.no);
>
> sw.start;
> auto ca = chromAdapt(image, srcIlluminant,
> Illuminant.d65).toUnsigned;
> sw.stop;
>
> auto timeTaken = sw.peek.split!("seconds", "msecs");
> writefln("%d.%d seconds", timeTaken.seconds,
> timeTaken.msecs);
>
> auto c = new Compressor();
> ubyte[] jpegOutput;
> bool compressed = c.compress(ca.field, jpegOutput, width,
> height, 90);
>
> if (!compressed)
> {
> c.errorInfo.writeln;
> return;
> }
>
> "image-out.jpg".write(jpegOutput);
> }
> ```
>
> Functions found through profiling to be taking most time:
> * Chromatic adaptation:
> https://github.com/kyleingraham/photog/blob/up-chromadapt-perf/source/photog/color.d#L354
> * RGB to XYZ:
> https://github.com/kyleingraham/photog/blob/up-chromadapt-perf/source/photog/color.d#L142
> * XYZ to RGB:
> https://github.com/kyleingraham/photog/blob/up-chromadapt-perf/source/photog/color.d#L268
>
> A profile for the test code is
> [here](https://github.com/kyleingraham/photog/files/6274974/trace.zip). The trace.log.dot file can be viewed with xdot. The PDF version is [here](https://github.com/kyleingraham/photog/files/6275358/trace.log.pdf). The profile was generated using:
>
> * Compiled with dub build --build=profile --compiler=ldmd2
> * Visualized with profdump - dub run profdump -- -f -d -t 0.1
> trace.log trace.log.dot
>
> The branch containing the optimized code is here:
> https://github.com/kyleingraham/photog/tree/up-chromadapt-perf
> The corresponding release is here:
> https://github.com/kyleingraham/photog/releases/tag/v0.1.1-alpha
>
> If you've gotten this far thank you so much for reading. I hope
> there's enough information here to ease thinking about
> optimizations.
I am away from a proper dev machine at the moment so I can't
really delve into this in much detail, but some thoughts:
Are you making the compiler aware of your machine? Although the
obvious point here is vector width (you have AVX-512 from what I
can see, however I'm not sure if this is actually a win or not on
Skylake W), the compiler is also forced to use a completely
generic scheduling model which may or may not yield good code for
your procesor. For LDC, you'll want `-mcpu=native`. Also use
cross module inlining if you aren't already.
I also notice in your hot code, you are using function contracts:
When contracts are being checked, LDC actually does use them to
optimize the code, however in release builds when they are turned
off this is not the case. If you are happy to assume that they
are true in release builds (I believe you should at least), you
can give the compiler this additional information via the use of
an intrinsic or similar (with LDC I just use inline IR, on GDC
you have __builtin_unreachable()). LDC *will* assume asserts in
release builds as far as I have tested, however this still
invokes a branch (just not to a proper assert handler).
Via `pragma(inline, true)` you can wrap this idea into a little
"function" like this
```D
pragma(LDC_inline_ir)
R inlineIR(string s, R, P...)(P);
pragma(inline, true)
void assume()(bool condition)
{
//flesh this out if you want actually do something in debug
builds.
if(!condition)
{
inlineIR!("unreachable;", void)();
}
}
```
So you call this with information you know about your code, and
the optimizer will assume that the condition is true - in your
case it looks like the arrays are supposed to be of length 3, so
if you let the optimizer assume this it can yield a much much
much better function because of it (Compilers these days will
usually generate a big unrolled loop using the full vector width
of the machine, and some little ones that - if you put your
modular arithmetic hat on - make up the difference for the whole
array, but in your case you could just want 3 movs maybe). Using
this method can also yield much more aggressive autovectorization
for some loops.
Helping the optimizer in the aforementioned manner will often
*not* yield dramatic performance increases, at least in
throughput, however they will yield code that is smaller and has
a simpler control flow graph - these two facts are kind to
latency and your instruction cache (The I$ is quite big in 2021,
however processors now utilise a so-called L0 cache for the
decoded micro-ops which is not very big and can also yield
counterintuitive results on - admittedly contrived - loop
examples due to tricks that the CPU does to make typical code
faster ceasing to apply). I think AMD Zen has some interesting
loop alignment requirements but I could be misremembering.
As for the usual performance traps you'll need to look at the
program running in perf, or vTune if you can be bothered (vTune
is like a rifle to perf's air pistol, but is enormous on your
disk, it does however understand D code pretty well so I am
finding it quite interesting to play with).
More information about the Digitalmars-d
mailing list