oscbyspro / Numberick

An arithmagick overhaul in Swift
Apache License 2.0
15 stars 3 forks source link

Dynamic radix solution #78

Closed oscbyspro closed 1 year ago

oscbyspro commented 1 year ago

I have this side-quest-y goal of reducing the following type of generics to a single compiled specialization (while allowing more for development purposes). The dynamic NBKTwinHeaded<Base> (#72) combines Base and ReversedCollection<Base> in a sufficiently efficient manner, but that still leaves two types of radices and two specializations. I haven't tried it yet, but I believe that switching on an enum (or similar) should be viable.

@inlinable func integerTextUnchecked(
chunks: some RandomAccessCollection<UInt>, 
radix: some _NBKRadixUIntRoot, 
alphabet: MaxRadixAlphabetEncoder, 
prefix: NBK.UnsafeUTF8, 
suffix: NBK.UnsafeUTF8) -> String
oscbyspro commented 1 year ago

Wrapping the two models in an enum makes encoding ~(2.04, 1.50)x slower, but branching on power.isZero seems viable.

// MARK: Status Quo

[D10] passed (0.020 seconds)
[D16] passed (0.018 seconds)
[E10] passed (0.055 seconds)
[E16] passed (0.023 seconds)

// MARK: With AnyRadixUIntRoot (deleted Perfect & Imperfect & Protocol)

[D10] passed (0.020 seconds)
[D16] passed (0.018 seconds)
[E10] passed (0.055 seconds)
[E16] passed (0.027 seconds) // 0.028 when non-generic & non-@inlinable
oscbyspro commented 1 year ago

I mean, a dedicated hex formatter is naturally faster so optimizing for size when the difference is small seems reasonable.

oscbyspro commented 1 year ago

By loop unswitching a small part of it, the dynamic algorithm performs as well as multiple generic specializations. Nice.

oscbyspro commented 1 year ago

As it turns out, ! is surprisingly great. Dunno if it adds some kind of compiler hint, but init?()! seems like a viable alternative to init(unchecked:) in most situations. I'm changing that while I'm at it, and I'm making the radix model generic over the size of each chunk because why not? Sounds neat.

oscbyspro commented 1 year ago

A quick recap

[D10] passed (0.020 seconds)
[D16] passed (0.018 seconds)
[E10] passed (0.053 seconds) -0.02
[E16] passed (0.025 seconds) +0.02
func integerTextUnchecked(
chunks:   NBKTwinHeaded<NBK.UnsafeWords>,  
radix:    AnyRadixSolution<Int>,
alphabet: MaxRadixAlphabetEncoder, 
prefix:   UnsafeUTF8, 
suffix:   UnsafeUTF8) -> String
oscbyspro commented 1 year ago

It seems like the difference in performance comes from whether the first-chunk-method gets inlined in the main method. In case it matters.

oscbyspro commented 1 year ago

Here's something neat that seems somewhat viable, and by that I mean that it performs well but the associated divisor loses its type constraint (immediately punished for making it generic). Perhaps it's possible to do something similar with a macro?

@inlinable func unswitch(_ body: (any NBK.RadixSolution<Size>) -> Void) {
    switch self.power.isZero {
    case  true: body(NBK  .PerfectRadixSolution(self)!)
    case false: body(NBK.ImperfectRadixSolution(self)!) 
    }        
}

It works when you have a generic method, but they are the ones I want to remove :expressionless:

Edit: this approach viable, but marking it as throws/rethrows makes it slow.

radix.unswitch({ generic($0) })

Unswitching without error-prone copy-past would be nice. I imagine something like:

#unswitch(radix) {/* it looks like one method body, but it's actually two! */}
oscbyspro commented 1 year ago

I haven't played with macros much, but pure text transformations would allow me to drop two protocols: RadixSolution and RadixSolutionDivisior. I use them to avoid copy-pasta, but I think an AnyRadixSolution unswitch would be a viable alternative.

Edit: it would have to be a pure text transformation, absent generic constraints.

Edit: a cool thing about transforming text is that you can call different non-generic overloads. Hm (:bulb:).

oscbyspro commented 1 year ago

Perhaps I can trim complexity by replacing the divisor models with a closure. Hm.

func division() -> (Element) -> QR<Element, Element>

Wow. Would you look at that. The any unswitch method works with closures :star_struck:

Edit: but, it's way too slow. So sad :disappointed:

Edit: it's also possible to unswitch the divisor if it's the closure argument, but it's still slow :disappointed:

oscbyspro commented 1 year ago

Hm. I just think that adding this kind of protocol creates an unnecessary project dialect.

public protocol Map<Input, Output> {

    associatedtype Input

    associatedtype Output

    func callAsFunction(_ input: Input) -> Output
}

On a side note, however, I have a private project built around it and it's Sliced Bread 2.0™