Teaser: Practical Multithreading

Before I go offline for the end of year, I have a teaser for a blog post I’m preparing on multithreading.

In short, if I have this non-trivial processing (which I quickly whipped up for the example):

//
//  Created by Pierre Lebeaupin on 13/12/2020.
//  Copyright © 2020 Pierre Lebeaupin. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

import Foundation;

enum Op: String {
    case additive = "+", multiplicative = "*";
    
    func neutralValue() -> UInt32 {
        return ((self == .additive) ? 0 : 1);
    }
    
    func combine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a + b) : (a * b));
    }

    func uncombine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a - b) : (a / b));
    }
}


struct ReadyValue {
    let value: UInt32;
    let op: Op?;
    let description: () -> String;
    // Note that as a closure, it can depend on more than the stored properties, contrary to a
    // computed property.

    init(_ inValue: UInt32) {
        value = inValue;
        op = nil;
        description = { return inValue.description;};
    }
    
    init(value inValue: UInt32, op inOp: Op, description indescription: @escaping () -> String) {
        value = inValue;
        op = inOp;
        description = indescription;
    }
}

func resolve(_ startGoal: UInt32, _ incb: @escaping (_ result: String) -> Void, _ primitives: UInt32...)
{
    var l = [ReadyValue]();
    var composedCount = 0;
    var otherComposedCount = 0;
    
    var exploreAdditionOfRightNode = { (_ kind: Op) -> Void in return; };
    exploreAdditionOfRightNode = { (_ kind: Op) -> Void in
        if (l.count != 0) && (l[l.count - 1].op != kind) {
            guard otherComposedCount == 0 else {
                return;
            }
            
            otherComposedCount = composedCount;
            composedCount = 0;
        }
        defer {
            if (l.count != 0) && (l[l.count - 1].op != kind) {
                composedCount = otherComposedCount;
                otherComposedCount = 0;
            }
        }

        var decomposedValue = [false: kind.neutralValue(), true: kind.neutralValue()];
        
        var iteratePossibleLeftNodes = { (_ startingFrom: Int, _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void) -> Void) -> Void in return; }
        iteratePossibleLeftNodes = { (_ startingFrom: Int, _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void) -> Void) -> Void in
            for candidateOffset in startingFrom ..< (l.count - composedCount) {
                let rightChild = l.remove(at: candidateOffset);
                if let _ = rightChild.op {
                    otherComposedCount -= 1;
                }
                defer {
                    if let _ = rightChild.op {
                        otherComposedCount += 1;
                    }
                    l.insert(rightChild, at: candidateOffset);
                }

                for phase in 0...1 {
                    let reverse = (phase == 1);
                    { (_ valueComponent: inout UInt32) in
                        valueComponent = kind.combine(valueComponent, rightChild.value);
                    }(&decomposedValue[reverse]!);
                    defer {
                        { (_ valueComponent: inout UInt32) in
                            valueComponent = kind.uncombine(valueComponent, rightChild.value);
                        }(&decomposedValue[reverse]!);
                    }
                    
                    let selfNode = {(_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void) -> Void in
                        action(rightChild, reverse);
                        walkOperands(action);
                    };
                    
                    iteratePossibleLeftNodes(/* starting from */ candidateOffset, selfNode);
                    
                    // close current composition
                    guard ({
                        var num = 0;
                        selfNode({_,_ in num += 1;});
                        return num;
                    }() > 1) && ( (kind == .additive) ? decomposedValue[false]! > decomposedValue[true]! :
                                    ((decomposedValue[false]! % decomposedValue[true]!) == 0) ) else {
                        continue;
                    }
                    
                    let realizedValue = kind.uncombine(decomposedValue[false]!, decomposedValue[true]!);
                    let description = { () -> String in
                        var current = "(";
                        selfNode({(_ value: ReadyValue, _ freverse: Bool) -> Void in
                            current += " ";
                            current += (freverse ? (kind == .additive ? "-" : "/") : kind.rawValue);
                            current += " ";
                            current += value.description();
                        });
                        current += ")";
                            
                        return current;
                    };
                    
                    guard l.count > 0 else {
                        if realizedValue == startGoal {
                            incb(description());
                        }
                        continue;
                    }

                    composedCount += 1;
                    l.append(ReadyValue(value: realizedValue, op: kind, description: description));
                    defer {
                        l.remove(at: l.count - 1);
                        composedCount -= 1;
                    }

                    exploreAdditionOfRightNode(.additive);
                    exploreAdditionOfRightNode(.multiplicative);
                }
            }
        };
        
        iteratePossibleLeftNodes(/* starting from */ 0, { _ in return;});

        // Why, I must ask… Why? Why why why?
        iteratePossibleLeftNodes = { _,_ in return;};
    };

    for element in primitives {
        l.append(ReadyValue(element));
    }

    exploreAdditionOfRightNode(.additive);
    exploreAdditionOfRightNode(.multiplicative);

    // Why, I must ask… Why? Why why why?
    exploreAdditionOfRightNode = { _ in return;};
}

(try it out, e.g. with resolve(70, {print($0);}, 5, 4, 3, 2, 2);)

Given this code, I should be able to parallelize it with these simple changes (changes in bold):

//
//  Created by Pierre Lebeaupin on 13/12/2020.
//  Copyright © 2020 Pierre Lebeaupin. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

import Foundation;

enum Op: String {
    case additive = "+", multiplicative = "*";
    
    func neutralValue() -> UInt32 {
        return ((self == .additive) ? 0 : 1);
    }
    
    func combine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a + b) : (a * b));
    }

    func uncombine(_ a: UInt32, _ b: UInt32) -> UInt32 {
        return ((self == .additive) ? (a - b) : (a / b));
    }
}


struct ReadyValue {
    let value: UInt32;
    let op: Op?;
    let description: () -> String;
    // Note that as a closure, it can depend on more than the stored properties, contrary to a
    // computed property.

    init(_ inValue: UInt32) {
        value = inValue;
        op = nil;
        description = { return inValue.description;};
    }
    
    init(value inValue: UInt32, op inOp: Op, description indescription: @escaping () -> String) {
        value = inValue;
        op = inOp;
        description = indescription;
    }
}

func resolve(_ startGoal: UInt32, _ queue: DispatchQueue, _ incb: @escaping (_ result: String) -> Void, _ primitives: UInt32...) async
{
    var l = [ReadyValue]();
    var composedCount = 0;
    var otherComposedCount = 0;

    var isParent = true;
    
    let exploreAdditionOfRightNode = { (_ kind: Op) async -> Void in
        if (l.count != 0) && (l[l.count - 1].op != kind) {
            guard otherComposedCount == 0 else {
                return;
            }
            
            otherComposedCount = composedCount;
            composedCount = 0;
        }
        defer {
            if (l.count != 0) && (l[l.count - 1].op != kind) {
                composedCount = otherComposedCount;
                otherComposedCount = 0;
            }
        }

        var decomposedValue = [false: kind.neutralValue(), true: kind.neutralValue()];
        
        let iteratePossibleLeftNodes = { (_ startingFrom: Int, _ walkOperands: @escaping (_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void) -> Void) async -> Void in
            var isChildStart = false;

            if isParent {
                let cost = operationsToExplore(l.count, composedCount, otherComposedCount, startingFrom, walkOperands);

                if cost < maxCost && cost > minCost {
                    guard await fork == .child else {
                        return;
                    }

                    isParent = false;
                    isChildStart = true;
                }
            }
            defer {
                if isChildStart {
                    async exit;
                }
            }
                

            for candidateOffset in startingFrom ..< (l.count - composedCount) {
                let rightChild = l.remove(at: candidateOffset);
                if let _ = rightChild.op {
                    otherComposedCount -= 1;
                }
                defer {
                    if let _ = rightChild.op {
                        otherComposedCount += 1;
                    }
                    l.insert(rightChild, at: candidateOffset);
                }

                for phase in 0...1 {
                    let reverse = (phase == 1);
                    { (_ valueComponent: inout UInt32) in
                        valueComponent = kind.combine(valueComponent, rightChild.value);
                    }(&decomposedValue[reverse]!);
                    defer {
                        { (_ valueComponent: inout UInt32) in
                            valueComponent = kind.uncombine(valueComponent, rightChild.value);
                        }(&decomposedValue[reverse]!);
                    }
                    
                    let selfNode = {(_ action: (_ value: ReadyValue, _ reverse: Bool) -> Void) -> Void in
                        action(rightChild, reverse);
                        walkOperands(action);
                    };
                    
                    await iteratePossibleLeftNodes(/* starting from */ candidateOffset, selfNode);
                    
                    // close current composition
                    guard ({
                        var num = 0;
                        selfNode({_,_ in num += 1;});
                        return num;
                    }() > 1) && ( (kind == .additive) ? decomposedValue[false]! > decomposedValue[true]! :
                                    ((decomposedValue[false]! % decomposedValue[true]!) == 0) ) else {
                        continue;
                    }
                    
                    let realizedValue = kind.uncombine(decomposedValue[false]!, decomposedValue[true]!);
                    let description = { () -> String in
                        var current = "(";
                        selfNode({(_ value: ReadyValue, _ freverse: Bool) -> Void in
                            current += " ";
                            current += (freverse ? (kind == .additive ? "-" : "/") : kind.rawValue);
                            current += " ";
                            current += value.description();
                        });
                        current += ")";
                            
                        return current;
                    };
                    
                    guard l.count > 0 else {
                        if realizedValue == startGoal {
                            let solution = description();
                            queue.async { incb(solution); };
                        }
                        continue;
                    }

                    composedCount += 1;
                    l.append(ReadyValue(value: realizedValue, op: kind, description: description));
                    defer {
                        l.remove(at: l.count - 1);
                        composedCount -= 1;
                    }

                    await exploreAdditionOfRightNode(.additive);
                    await exploreAdditionOfRightNode(.multiplicative);
                }
            }
        };
        
        await iteratePossibleLeftNodes(/* starting from */ 0, { _ in return;});
    };

    for element in primitives {
        l.append(ReadyValue(element));
    }

    await exploreAdditionOfRightNode(.additive);
    await exploreAdditionOfRightNode(.multiplicative);
}

(The implementation of operationsToExplore() is left as an exercise for the reader).

Details in the forthcoming post…

Leave a Reply

Name *
Email *
Website