Porting Graphing Calculator from C++ to Swift
swift.org
swift.org
The origin story is cool as heck, too. As discussed many many times on HN: http://pacifict.com/Story/
It’s been a while, but I wrote a few different “slow start” functions for a load balancer and used Graphing Calculator to not only illustrate the behavior, but also show which parameters were tunable.
I hope he decides to also open source the C++ version. It would be interesting to be able to see both versions, and to compare them.
Perhaps one of the greatest benefits of open sourcing both versions, that I can see, is that others could then look at the two versions of Ron’s code for inspiration on how to go about porting their own legacy applications from C++ to Swift.
Aside from that, it also provides historical context that future programmers and future programming language creators could learn from. Both in terms of good things and any of the lesser good sides.
The author is being kind. Once outside the prescribed path, you’re in deep trouble. And that’s the thing with declarative code. There must be a great escape hatch. The framework can’t foresee all possible use cases.
> There must be a great escape hatch.
or you wait for the next os release (which really hurts adoption and dirties code with lots of workarounds)Apple is very much in the camp of maybe tolerating third parties, but only so long as they only ever do stuff in the bounds of what Apple wants them to do
Apple certainly has done a much better job of picking a development strategy and iterating on it than Microsoft has over the past couple of decades.
Apple could really use some good competition, because Windows and desktop Linux aren’t enough.
This is impressive.
When people make 'size' comparisons, I sometimes wonder if they'd best be done using lzip or something on the two code bases -- because the compressors are trying to remove redundancy in coding, and if one uses lots of whitespace in C++ to enhance readability, this should not punish C++ in the size department.
You generally want some 'system' ('language' seems too restrictive) to enable one to capture the algorithms rapidly and accurately in some notation (again, 'language' seems not quite the right word), and to as quickly as possible convert said notation into working binary (compiler, interpreter, JIT, etc all fair game) that satisfies the user need.
I claim (wrongly?) that these days, we generally only need a decent 'glue' notation that allows us to mash together existing libraries/classes/etc to produce the final product. Some of this glue would be sort-of templates, lispish macros, some would just be e.g. an FFT routine. But only by burying complexity in well-tested, lower-level chunks will we be able to build big systems that don't collapse.
Sorry, enough soapbox. I guess I'm suggesting that it's interesting to compare Swift to C++. It sort of begs the question: what is 'the best' way to get some functionality (e.g. this cool calc app) written?
https://programming-language-benchmarks.vercel.app/swift-vs-...
It's really hard not to like reducing code size to 30% of C++ for long term maintenance IMHO.
I spent 15 years doing C++ and read so many books but always felt the language would get in the way. When switching to the Swift stuff just worked the way it should. No need to read tons of books to find clever tricks to get around inherent limitations of C++.
C++ was good for it’s time but it is strange that people think one cannot radically improve on what C++ is with newer languages. It is tied down with backwards compatibility with a 50 year old language.
That had lots of advantages, but downsides has caught up with advantages.
I suspect [Ancient C/C++] weighed down with tech debt and utilising a plethora of old-code frameworks to Swift is what let him save 30%. If he did the port to Modern C++ and modern libs, he would have likely saved 50%.
[[nodiscard]] constexpr int Add(const int& a, const int& b) const noexcept { return a + b; }
Modern C++ does help with most type specification, looping over containers, writing constructors and making value classes. It's definitely not all bad, but there's still a lot of boilerplate to C++.
> C++ requires you to duplicate signatures in header files, which that alone requires a good degree of boilerplate.
I get that people stick to C++ because it is too costly to port. But it doesn’t make a lot of sense to stick to C++ if you got the option to use Swift, Rust or Go.
Swift avoids null pointers, undefined static initializations and de-initialization order, virtual call disasters when calling a virtual from a constructor. Protocol oriented programming solves a lot of problems you tend to run into in C++ when doing object-oriented programming.
Template specification used to need a lot of decoration but with auto most of that is gone.
I suspect he just wanted to learn Swift and that's a good reason enough to make this rewrite, as he admits in the final paragraphs of the article: "I’ve enjoyed learning Swift and am much happier with the state of the code now"
I would describe dealing with those issues as "boilerplate" because it's just a lot of code you should not have to write.
Swift is so much more comfortable and fun.
(btw I still love C++ and cross platform support completely flips the script)
I'm not sure we're thinking of the same standard library if you would describe it as thin...[0]
[0]: https://docs.microsoft.com/en-us/cpp/standard-library/cpp-st...
I honestly don't know what else you would be looking for in a standard library. C++ provides out of the box:
- hash maps
- sets
- dynamic arrays
- fixed length arrays
- bit sets
- option types
- atomics
- mutexes
- futures
- strings
- queues/stacks/Deques
- tuples
- variants
- streams
- etc
In addition, they have a huge range of algorithms that operate on all of these containers. I'm really curious, what does the standard library not have that you think forces you to write boilerplate?
- Unified result types. C++23 is finally going to have `std::expected`, but it will probably take another version or two to see the `std::expected`-only version of standard library.
- Iterator utilities. Again, C++23 will see a significant improvement in <ranges> but that still falls short of what Rust `Iterator` provides by default.
- Checked arithmetic. Rust and many other languages distinguish checked and unchecked arithmetic and put one of them (typically unchecked one) into the standard library. C++ doesn't have anything like that.
- Guarded mutex (that is, a portion of memory by design only accessible when locked) and concurrent queue. C++ does support concurrency out of the box, but its tools are pretty bare bones and this is one example.
Just a couple other points though, C++ provides fully customizable iterators. They don't have ranges, but ime the amount of times I need ranges vs just iterating over a container are relatively low. Also, is a lock guard in C++ not the same thing as a guarded mutex? This may be ignorance on my part because I'm not familiar with rust, but it sounds like the same thing to me. A lock guard will lock and automatically release a mutex once it goes out of the current scope.
What I find most lacking from the C++ standard library is actually coherence. Different parts of standard library were developed separately from each other, and while adapters between them exists (e.g. stream iterators can be used with `std::format_to`) they are by definition boilerplates. Some parts are simply not relevant in the modern C++ programming and still have to be retained for compatibillity (e.g. `std::auto_ptr`). In this view C standard library is actually a lot more coherent than C++, even though C standard library itself is less powerful and featureful than C++.
> They don't have ranges, but ime the amount of times I need ranges vs just iterating over a container are relatively low.
I think this is simply because C++ iterators are less usable. All iterators can be technically rewritten as loops, so you may have internalized to avoid using iterators when iterators would be beneficial but difficult to use. I personally find myself using iterators a lot more in Rust than in C++.
> Also, is a lock guard in C++ not the same thing as a guarded mutex? This may be ignorance on my part because I'm not familiar with rust, but it sounds like the same thing to me. A lock guard will lock and automatically release a mutex once it goes out of the current scope.
I actually don't know the exact term (the name "guarded mutex/lock" was what I've used in the past), but I meant a mutex that is strongly coupled with some additional data so that the API prevents or at least discourages to access the data without acquiring the mutex. In a hypothetical design `std::mutexed<T>` will be `T` plus `std::mutex` and `std::lock_guard<std::mutexed<T>>` would have `operator *` which allows the access to `T`. In Rust [1] this is actually the default, and you never have a bare `std::mutex` (though it can be simulated with placeholder data, `Mutex<()>`).
Gotcha, I appreciate the back and forth which helped me to clarify that point a bit better :)
> What I find most lacking from the C++ standard library is actually coherence.
I 100% agree. There are plenty of areas that C++ is very lacking, and I think this comment highlights that.
I initially started this thread off because a big complaint I hear among C++ users is the language is a bloated mess, which is why the parent comment saying that C++ had a thin standard library didn't ring true to me. But I think what you're saying here is closer to the truth. It has a lot of good features, but they're mostly after thoughts and poorly integrated. Whereas a language like Rust (or another modern language) has around the same amount of features builtin, but they're more focused, integrated, and planned :)
What should clearly be outside is stuff like graphics library, GUI, HTML.
C++ does provide all of this in the standard library except for sockets, Json and XML. I think once you get to things like Json and XML it doesn't make much sense to include that in the standard library. There are plenty of applications that don't need or use either of those, and it's a shame to bundle some configuration language when somebody may have another preference like yaml or something. There's nothing wrong with providing additional standard add-ons that include these, but I definitely don't think it makes a lot of sense to bundle it in with the standard.
This is a whole separate conversation, but my main point was there are a lot of better things to pick on C++ for. Most complaints I hear are that it includes too many useless features haha.
The streams from the standard library are broken beyond repair, I avoid them at all costs: https://www.moria.us/articles/iostream-is-hopelessly-broken/ Even <stdio.h> C header is much better abstraction, despite 50 years old.
To be useful, option types and tuples need language support. C++ doesn't have it.
Similarly, to be useful, futures need runtime support i.e. asynchronous I/O. They also need language support like async-await, to generate these state machines. C++ has none of them.
The standard library still does not really support Unicode, people need third-party libraries like ICU just to manipulate strings: https://stackoverflow.com/q/42946335/126995
C++:
int first[] = {1,2,3,4};
int second[] = {5,6,3,4};
std::vector<int> v(8);
std::vector<int>::iterator it;
std::sort (first,first + 4); // 1 2 3 4
std::sort (second,second + 4); // 3 4 5 6
it = std::set_union (first, first + 4, second, second + 4, v.begin());
v.resize(it - v.begin());
for(it = v.begin(); it != v.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
Swift: let a = Set([1,2,3,4])
let b = Set([5,6,3,4])
let c = a.union(b)
print(c) //prints "[1, 2, 3, 5, 6, 4]" std::set a{1,2,3};
std::set b{5,6,3,4};
std::set c{a};
c.insert(b.begin, b.end());
for(auto x: c)
std::cout<<c<<' c;
std::cout << '\n'; #include <algorithm>
#include <iterator>
#include <vector>
#include <iostream>
using namespace std::ranges;
int main()
{
std::vector a = {1,2,3,4};
std::vector b = {5,6,3,4};
sort(a);
sort(b);
decltype(a) c;
set_union (a, b, back_inserter(c));
copy(c, std::ostream_iterator<int>(std::cout, "\n"));
}
https://gcc.godbolt.org/z/44Ksf1hK8Note the value type deduction when declaring the vectors, the use of back_inserter to avoid having to resize the result (c) vector, the use of std::ranges to do away with all the begin + end crapola, and the way ranges and iterators are inter-mixed.
C++ is always going to be a bit more verbose in trivial cases, but it's not that bad, and exposes a lot more flexibility in complex ones.
For example, a few areas where Swift reduces boilerplate and the syntax it provides to do so:
- a complete null/optional story: first class optional types (T? = Option<T>), null chaining (obj?.prop), and null coalescing (T ?? "default")
- error handling: fn() throws Err (= fn() -> Result<T,Err>), try, try?, try!, do/catch (out-of-band error handling), w/o stack unwinding, see nulls
- and struct/enum lifecycle management: constructors, getter/setter props, defaults, named functions, named tuples, etc. (this list is loong)
Individually each feature is small and could possibly be argued as bloat or overhead since you can accomplish the same with just regular functions and Rust-like code and abstractions, but combined they make the language feel more expressive (and more fun IMO) than would otherwise be possible and often without sacrificing performance. They also serve to reduce cognitive overhead in the same way using for loops is nicer than desugaring to while loops. This is nothing to say of the syntax itself, and a bunch of other small features; Swift really is a case where the whole is greater than the sum of of its parts.Can you say more about the parse tree impact on performance?
Is the expression left in a tree form and effectively "interpreted" from the AST for all points? Is that the critical path?
My basic question would be: why not go back to flex/bison/yacc/whatever via C-FFI? (But I think it would still be bad, since you'll want to get to a Swift data structure for your ops and those will still have the Arc issues)
I did investigate maintaining the flex/bison parser, since its generated state machine C code is more robust than my handwritten recursive descent parser when presented with pathological input. However, as you say, since I need a Swift data structure in the end, there is little to be gained and a lot of complication bridging via a C-FFI.
Another question, have you tried using Accelerate framework to solve performance bottlenecks (or save yourself from having to write your own calc code)?
In principle, I could get rid of reference counting overhead by using value types or immutable data. I couldn't see a simple path to doing that without re-architecting everything (with no guarantee that the end result would not just have different performance issues.) For the moment, I'm awaiting compiler improvements before re-evaluating the tradeoffs. There is certainly room for the compiler to reason better on eliding retain/release. https://github.com/apple/swift/issues/58549
Yes, the code does use Accelerate where applicable. That is one component of the numeric evaluation. It addresses the lowest level of things like evaluate the sin function on every array element, or multiply there arrays element wise. Performance tuning is a game of whack-a-mole. There's always another bottleneck somewhere.
(I read the article hours ago and didn't notice you'd posted. Btw, I think we would have gotten better conversation if you had said "Hey, this is me! AMA" as a top-level comment)
https://github.com/apple/swift/tree/main/docs/CppInteroperab...
https://forums.swift.org/t/swift-and-c-interoperability-work...
*Objective-C++
To be honest, swift feels more like a bad parody on Scala.