Multiplication in Brainfuck - minimum number of evaluations

3

Given two numbers in tape location #0 and tape location #1 in Brainfuck, compute their product into another tape location (specify with answer). Optimize for the least amount of time-units used, where the interpretation of any command uses one time-unit. As example, here is a quick C++ program to determine the number of time-units used by a Brainfuck program:

#include <iostream>
#include <fstream>
#include <stack>
#include <algorithm>
#define MEMORY 10000
#define DATATYPE uint32_t
#define ALWAYSPOSITIVE true

DATATYPE tape[MEMORY];
int loc, instp;

int main(int argc, char* argv[]) {
    std::string infile = "a.bf";
    bool numeric = false;
    bool debug = false;
    std::string flag;
    for(int i=1; i<argc; i++) {
        std::string arg = argv[i];
        if(arg[0] == '-') flag = arg;
        else {
            if(flag == "-i" || flag == "--in") infile = arg;
        }

        if(flag == "-n" || flag == "--numeric") numeric = true; 
        if(flag == "-d" || flag == "--debug") debug = true;
    }
    if(infile == "") {
        std::cerr << "Fatal: no input file specified. Usage: ./pp2bf -i <file> [-n] [-d]" << std::endl;
        return 1;
    }
    std::ifstream progf(infile);
    std::string prog; std::string line;
    std::stack<int> loop;
    while(progf >> line) prog += line;
    instp = 0; uint64_t instc = 0, ptmove = 0, mchange = 0;
    int maxaccessedmem = 0;
    while(instp != prog.size()) {
        char inst = prog[instp];
        if(debug) {
            std::cout << inst << " #" << instp << " @" << loc;
            for(int i=0; i<=maxaccessedmem; i++)
                std::cout << " " << tape[i];
            std::cout << "\n";
        }
        if(inst == '[') {
            if(!tape[loc]) { // automatically advance to matching ]
                int n = 1;
                for(int i=instp+1;;i++) {
                    if(prog[i] == '[') n++;
                    if(prog[i] == ']') n--;
                    if(n == 0) {
                        instp = i+1;
                        break;
                    }
                }
                continue;
            }
            loop.push(instp);
        } else if(inst == ']') {
            if(tape[loc]) {
                instp = loop.top() + 1;
                continue;
            } else {
                loop.pop();
            }
        } else if(inst == '>') {
            loc++;
            maxaccessedmem = std::max(maxaccessedmem, loc);
            ptmove++;
            if(loc == MEMORY) {
                std::cerr << "Fatal: Pointer out of bounds (try increasing memory) @ instruction " << instp << std::endl;
                return 1;
            }
        } else if(inst == '<') {
            loc--;
            ptmove++;
            if(loc == -1) {
                std::cerr << "Fatal: Pointer out of bounds @ instruction " << instp << std::endl;
                return 1;
            }
        } else if(inst == '+') {
            mchange++;
            tape[loc]++;
        } else if(inst == '-') {
            mchange++;
            if(tape[loc] == 0) {
                if(ALWAYSPOSITIVE) {
                    std::cerr << "Fatal: Negative tape value at " << loc << " @ instruction " << instp << std::endl;
                    return 1;
                } else {
                    std::cerr << "Warning: Tape value at " << loc << " @ instruction " << instp << std::endl;
                }
            }
            tape[loc]--;
        } else if(inst == '.') {
            mchange++;
            if(numeric) {
                std::cout << (DATATYPE)(tape[loc]) << std::endl;
            } else {
                std::cout << (char)(tape[loc]);
            }
        } else if(inst == ',') {
            mchange++;
            if(numeric) {
                int inp; std::cin >> inp;
                tape[loc] = inp;
            } else {
                char inp; std::cin >> inp;
                tape[loc] = inp;
            }
        }
        instp++;
        instc++;
    }
    std::cout << "\nTerminated after " << instc << " time-units." << \
                 "\nMoved pointer " << ptmove << " times." << \
                 "\nChanged a memory location " << mchange << " times." << std::endl;
}

Note: In this implementation the tape is allotted 10K locations and each tape is an unsigned 32-bit integer - not the 8 bits used in a standard Brainfuck implementation.

For starters, here's mine:

,>,[<[>>+>+<<<-]>>>[<<<+>>>-]<<-]>.<<

When you compile the C++ program as rbf, save the Brainfuck program as mult.bf, run ./rbf -i mult.bf -n, and type 1000 followed by pressing Enter twice, the following output appears:

1000000

Terminated after 17011009 time-units.
Moved pointer 12006004 times.
Changed a memory location 5001003 times.

Your goal is to minimize the first value (i.e. 17011009). In case of ties minimize the second (12006004) and then the third (5001003).

The standard test case is

1000
1000

However, your code shouldn't be optimized for this test case only, but expend minimal time-units for any two numbers.

EDIT: For well-formedness, and not hardcoding an answer, take the sum of the expended time units in all test cases

x
x

where x = 900...1000 inclusive to get the final time units expended.

w33z8kqrqk8zzzx33

Posted 2019-07-09T12:36:20.560

Reputation: 59

Question was closed 2019-07-11T05:45:20.667

3Welcome to the site! This is has the potential to be a good challenge there are just a couple of things I am concerned about. The first is how are loops timed? There are two ways that loops can happen that are identical in behavior but different in number of operations. The first is that the ] always puts the ip back to [ and [ does the checking and skipping. The second is that [ checks and skips the loop if zero while ] is continues on if zero and returns to the beginning of the loop if not. You ought to explain how exactly timing is scored. – Post Rock Garf Hunter – 2019-07-09T12:51:55.157

1Firstly, thank you very much for being interested in my question! How it is timed is that the first [ counts towards the time, and every ] is counted towards the time, including the one in which terminates the program. But I appear to have forgot that an address can be 0... oops! ;) – w33z8kqrqk8zzzx33 – 2019-07-09T12:55:01.083

2The second thing is: optimizing for time units used on it's own is not a sufficient winning criterion for us here. We require winning criteria to unambiguously rank answers. There should be no debate as to whether which answer is better or if the two answers are the same score. Your criterion does not quite do that. It is clear which answer is faster sometimes, but because there are answers that are faster for some inputs and slower for others how are those ranked? (My personal recommendation is to have answers get a concrete score by summing their scores across a number of specific cases) – Post Rock Garf Hunter – 2019-07-09T12:57:20.420

1I don't think that is possible, since increasing either input will increase the time units expended. But I will make it so that the inputs will always be 1000 and 1000. – w33z8kqrqk8zzzx33 – 2019-07-09T12:59:27.360

1I would warn against using a single input, it is possible to hardcode for that input so that the program first checks that the input is that number and then outputs a memorized result and multiplies any other two numbers normally. – Post Rock Garf Hunter – 2019-07-09T13:05:23.573

2Increasing with size of input is not necessarily in conflict with my last comment. Say for example I have two answers, answer A is faster or slower mostly dependent on the size of the first input (that is that large values here make it very slow but large values in the second have little effect), while B is the opposite being more dependent on the second input. B will be faster than a sometimes for example if given 10000, 4 but A will also be faster sometimes for example if given 4, 10000. There are all sorts of other ways for this to happen too (this is just an example). – Post Rock Garf Hunter – 2019-07-09T13:07:14.770

2Ah, yes: this is present in my solution, too (4, 1000 is ~79K time units whilst 1000,4 is ~68K time units). I'll consider on how to improve this. – w33z8kqrqk8zzzx33 – 2019-07-09T13:14:17.993

1Another thing that I am just realizing. Most brainfuck implementations wrap cells at 255 and some even wrap the tape at some point. What implementation of brainfuck are we expected to use? It seems obvious it is not the 255 wrapping version but your C++ implementation does seem to have wrapping albeit at a very high point. This is best to make explicit in the challenge. – Post Rock Garf Hunter – 2019-07-09T21:22:16.397

uint32_t is 0 to 4294967295 - wraps at 4294967296. – w33z8kqrqk8zzzx33 – 2019-07-10T01:24:08.640

It's not clear whether a specific tape size or cell size is required. – mbomb007 – 2019-07-10T15:32:49.827

I'd recommend adding all the information from your comments to the actual question itself, as well as specifying the intended brainfuck implementation (I assume infinite sized cells and no negative cells). Perhaps provide a reference implementation that can take a brainfuck program, test it is correct, and provide a score over your given range. – Jo King – 2019-07-11T05:45:16.707

Answers

2

7,009,011 timesteps for \$1000*1000\$

>>,>,[<[-<+<+>>]>-[<<[-<+>>+<]]>[>->]<]<<<.

Try It Online!

This increases the counter on both the initial transfer and the second transfer back to the original position. This makes it almost the same efficiency for both orderings of the input, for example, \$100*1000\$ is \$709011\$ steps, while \$1000*100\$ is \$700911\$ steps.

I'm not really sure I want to attempt calculating sum of the given test cases.

Jo King

Posted 2019-07-09T12:36:20.560

Reputation: 38 234