Swift v. Java…script?
Number crunching speed in JS is catching up to native code
Our recent test of using Emscripten to generate both Javascript and WebAsm code was so positive in terms of run-time performance numbers and ease of use that we thought we would extend the test of the fairly complicated Singular Value Decomposition [SVD] algorithm to the Swift language. Let’s see how Swift’s performance on this algorithm compares to our good results from JS and WebAsm!
For this test, we did not use an automatic translator or transpiler from C to Swift, and we tried to avoid doing anything too “C-like” such as using unsafe memory pointers and similar: https://theswiftdev.com/unsafe-memory-pointers-in-swift. That is, to start, at least…
We wanted it to be a real test with typical Swift code. As it turns out, this may have been a bad idea. Well… not entirely bad, as there was learning involved, but not optimal from a run time speed point of view, which we will get into in a moment. Forging ahead, we buckled down and translated the rather detailed Numerical Recipes [NR] SVDCMP routine into Swift 5 [Note 1], one line at a time, and many lines of code went over to Swift from C as-is. You can read more about this routine in our original Emscripten test.
Porting notes
Some concepts we followed when doing the manual port of NR SVDCMP (SVD compute) to Swift may be of interest:
1. Use “Array of Arrays” for the 2D matrices, of type Array<Array<Double>>(Swift-like) instead of the NR method of allocating a matrix (that is, allocating one contiguous block of data and setting up pointers to each row). It may be beneficial to allocate matrices the NR way, but, to start, we coded the way you would do it in Swift if you did not know any better.
2. Use ordinary Swift functions in place of (C-like) macros:
SQR (square with fast zero path)
MAX (of 2 Double values)
SIGN (of Double)
IMIN (was MIN) min of 2 Int values
(This may cause function call overhead vs. C unless the Swift optimizer inlines functions; as it turns out, performance hits seem to be more related to the array-of-arrays rather than these small functions).
3. Zero out arrays and matrices during allocation (may be a slight performance penalty) using the repeating:0 method in Swift, viz:
Array<Double>(repeating:0, count:n)
4. Use the stride( ) Swift technique for backward counting loops when we do not need to use the loop index after the loop is done, since C-style for loops are no longer supported in Swift.
int l; // “ell”
//for (l=k; l>=0; l--) // orig C
for l in stride(from: k, through: 0, by: -1) // Swift
Note: This particular loop over “ell” in the original code caused a bug on first port, since the C code in one case (that we know of so far) uses the loop index value after the loop is done. Since Swift loop index variables have their own scope, the higher scope “ell” variable was referenced after the loop instead of the “ell” variable that was counted down in the strided loop (that is, variable shadowing is/was occurring: https://en.wikipedia.org/wiki/Variable_shadowing).
We thought we remembered that the ObjC compiler gave warnings when you did something like this (variable of same name in nested scopes), but we did not see any Swift warnings of this condition, and it resulted in a bug that took a little while to track down. There may be a flag to set on the Swift compiler to show these warnings, but we did not know what this flag might be when we ran this test. This situation seems odd, since Swift seems to give a huge amount of warnings about other issues.
To remedy the problem, we replaced the strided loop with a while loop per a recommendation from stackoverflow. This gives the desired value for the ell variable if the inner loop code breaks out on conditions (which it does):
l = k
while (l >= 0)
{
// inner loop code that has
// conditional break statements
l -= 1 // added for while loop
}We have not yet re-reviewed the code to check for other cases where we might need the loop index to retain its loop-set value after the loop, but numeric results are looking good so far for the smaller cases, as we show later in this document. To avoid analyzing each original C for loop for these shadowing errors, it might be more expedient to use while loops as above to replace all for loops, but this we leave until (maybe) later. It seems possible to code the loop break such that the index variable is transferred to same name (different scope) outer variable through a temp variable. However, this is more than we want to deal with at the moment for this test setup.
5. There are some pieces of code in the original C that have patterns like:
x=-y; // set x to negative y
The problem seemed to be that, without white space separating the operator (as is sometimes found in older code like this NR routine), Swift treated this as a syntax error, so our initial thought was: Ah ha, the Swift crew must have gotten rid of the -= operator recently, like they (sadly?) got rid of ++ and its decrementing [Note 3] companion a while back. But no, it was merely an issue with white space, and we were misreading =- as -=, easily remedied (but not so easy to find; print statements are still your friend in Swift debugging).
6. Other findings: The C system routines drand48, srand48 and gettimeofday (system time routine) seemed to work okay (the last requiring a bit of Swift to C bridging due to pointers to C structs needed as arguments). Found on stackoverflow, this compiles and runs in Swift 5:
var stopTime:timeval = timeval(tv_sec: 0, tv_usec: 0)
gettimeofday(&startTime, nil)Numeric check on 3x3 random matrix
We first verified that the drand48 random number generator with a common seed gives the same random input matrix on both the C and Swift sides for this small test case. This was expected to be the situation, since we presumed that the Swift version merely calls down to the same system routine as the C version.
Then, we repeated our 3x3 matrix test that we did for WebAsm/JS to check numeric accuracy (see prior writeup), using the Swift print( ) function default Double value formatting, which seemed to have enough digits for checking. Dissimilar digits are shown in bold. As one can see (maybe not on mobile; there seems to be some issue with bold font in medium.com code blocks on mobile), the numeric accuracy of our port checks out to double precision for the small test case.
C ref results U matrix output (from A inout parameter)
+0.05098883372313427198 +0.72180521614029635735 +0.69021545099209058449
-0.97629077748557502314 -0.10955555066576420919 +0.18669198995921851281
-0.21037218597563298039 +0.68337018611509625643 -0.69910566590241007834Swift
0.05098883372313421 0.7218052161402965 0.6902154509920909
-0.9762907774855751 -0.10955555066576435 0.18669198995921835
-0.210372185975633 0.683370186115096 -0.6991056659024101Accuracy being spot-on, we proceeded to performance testing.
Get NTTP’s stories in your inbox
Join Medium for free to get updates from this writer.
We went to full optimization immediately on the Swift side, -O Optimize for Speed, since we were not concerned with memory or code size for this test.
800x800 matrix size runs in Swift: seemed to vary between 18.x to 20.x seconds.
C code equiv was about 4.1 to 4.2 seconds.
Recall from our prior article that JS gave performance of 6+ seconds for this 800x800 test. While we did not translate the C to JS code manually, we may want to go back and do this (or at least check the JS output of Emscripten), since we found at least one issue with variable shadowing here. Could it be that the auto-generated JS code was so fast because it wasn’t actually completing the algorithm due to some shadowed variable causing early termination? The fact that numerics seemed to check out well for smaller cases is a positive sign that there is Not a bug in the JS version, but due to the complexity of the algorithm, we cannot rule out a bug with larger matrix sizes.
Apple (in its Swift presentations) does mention that Swift is slower than plain C/C++ in some cases, so this part of the result is not surprising. However, it is surprising that our Javascript version seemed to beat Swift performance by a large margin, unless, as we said, there are some other bugs in the JS code that we haven’t noticed. Again, the fact that numerics checked out for the 3x3 case makes this unlikely though not impossible. Maybe we need a code coverage tool to make sure all the code branches get exercised? Or, maybe we need to run more numeric comparison tests with larger matrices.
Instruments to the rescue
Using Xcode Instruments’ time profiler, we can see that Swift Array element access seems to be causing much of the performance hit.
We may be able to return to optimizing this code as an exercise in optimizing Swift Arrays at some time in the near future, but we will leave the results as reported for now. For example, it might be more expedient (and run-time efficient) to just use an automatic ObjC converter such as the excellent swiftify.com [Note 2] to convert such C code to Swift, and have the converter figure the most efficient way. It looks like others have run into similar issues, so we will leave this for a separate optimization experiment:
https://github.com/apple/swift/issues/56451
https://www.fast.ai/posts/2019-01-10-swift-numerics.html
It may not make sense to manually optimize NR SVDCMP too much itself, since production code for this math is readily available in Apple’s system frameworks (see below). However, doing a little Swift re-coding of SVDCMP in that direction might lead to some insights.
Smaller size tests
We did not do timing tests of JS/WebAsm for smaller problems in our prior study, but thought we would do a quick check of them for C versus Swift while we were testing.
For smaller matrices, though we need to be more careful of time check overhead in this case, we can get approximate run times. These are single run tests. For more detailed trials, it would be best to use an “average of many” runs (which also makes any potential timer overhead less of an issue), so these results are merely a first approximation. Also, we would want to re-run this small n test after re-coding if we can find a way to speed up the above noted matrix element references.
n C Swift ~ratio(Swift/C)
10 43us 85us 2.0
50 1395us 3905us 2.8
100 10180us 29421us 2.9
200 75395us 283044us 3.8
us = microsecondsC is faster than Swift in all cases, which is not surprising.
Here, the n column indicates that we are sending matrices of size n*n to the SVD routine.
Production?
For production use on at least Apple systems, it is probably better to use the LAPACK routine DGESVD from Apple’s Accelerate framework and not the NR coded version described here. This study is more to check Swift speed for number crunching (and, as we now see, Array element read/write speed) compared to other languages in this study: C, JS, and WebAsm.
Apple seems to provide Swift bindings for these low level LAPACK routines:
https://developer.apple.com/documentation/accelerate/solving_systems_of_linear_equations_with_lapack
However, unless one is familiar with LAPACK calling conventions, the mental exertion to figure out
1) the meanings of all the function arguments,
2) column- vs. row-major ordered data, and
3) the use of contiguous memory blocks for matrices rather than arrays-of-arrays as we do in this example
can be a stumbling block to rapid prototyping, so it sometimes may be better to start with simple NR routines and then migrate to LAPACK versions for speed and possibly more accuracy once you have proven out your concepts.
The above Apple link mentions other LAPACK routines, but we have used the single precision version of this routine SGESVD from Objective C for generating principal components (for studying multi-asset correlation) in our MCarloRisk apps, so we know that they have that specific routine in their Accelerate library. One would hope that Apple provides Swift bindings for all of the LAPACK routines, but we have not verified this.
Summary
We see again the fairly surprising result of Emscripten auto-generated JS being notably faster than an approximately equivalent Swift version of a complicated algo, with the performance bottleneck seemingly due to Array element read/write… that is, if the JS is actually correct for larger matrix sizes, a contingency that we need to investigate. Could it be that performance-intensive code would run better in one of Apple’s WebKit WKWebViews or JavaScriptCore with their super-speed JS engines than in native compiled Swift? Of course there are user interface concerns (with native UI sometimes a better choice than a web view UI), and before going to a web view, it might be better to just re-do the Swift code with lower level memory block C-like arrays; and this test here is, after all, only one algorithm. But the performance we are seeing with JS does open up realistic options for deploying platform-independent, algorithm-intensive number crunching code in the (more portable?) JS, rather than having to drop into the less memory-safe C or C++ (-like) realms.
Notes
[Note 1] Whatever Swift version is available in Xcode 14.2
[Note 2] We used swiftify.com to convert our older code base Market Vector Auto Regression app from ObjC to Swift, and it does the job much faster than manual recoding. As computations are done on a remote server in Python, run-time performance was not an issue.
[Note 3] The medium.com editor seems to keep turning two minus signs in a row into a dash — an admirable feature for literary purposes, but not so admirable for code — so it is difficult to type the decrement operator.
