It's weird that reasoning through complex mathematical proofs is easier (for both me and GPT) than mentally evaluating a short snippet of code. I've done a BSCS, I've done those tests that ask you what the output of a poorly written and tricky loop is, that stuff hurts my brain. In comparison, a mathematical proof, once you're ready, feels good.
"Mentally evaluating a short snippet of code" requires remembering the state of that code, then repeatedly overwriting your memory as you step through it. GPT-4 isn't able to do that; it is in a sense purely functional.
I've had success getting it to evaluate FizzBuzz, but to do so I told it to write out the state of the program one timestep at a time.
This feels like an interesting insight into not just why LLMs might have trouble understanding some code, but also why people might have trouble understanding some code.
I wonder how gpt handles code where multiple objects of the same class call the same methods and each individual object calls the same methods with different threads as well as method local variables, class member variables, and static member variables are all in play. [Being fair to our LLM brethren; us meat sacks are sure to hate that hypothetical code as well.]
Off the top of my head, something like this comes to mind:
class X {
private static int _staticCount = 0;
private int _memberCount = 0;
public uint Stuff(int input) {
if (input <= 0) {
return (uint)(_staticCount + 1);
}
var y = new Thread(() => {
Interlocked.Add(ref X._staticCount, (int)Stuff(input - 1));
Interlocked.Add(ref _memberCount, (int)Stuff(input - 2));
});
y.Start();
y.Join();
return (uint)(Stuff(input - 1) + _memberCount);
}
}
class Program {
public static void Main() {
var x = new X();
var y = new X();
var z = new X();
uint resultX = x.Stuff(4);
uint resultY = y.Stuff(3);
uint resultZ = z.Stuff(2);
Console.WriteLine(resultX + resultY + resultZ);
}
}
EDIT: Probably should have setup another way to wait for the threads to end, that way all three X classes could be running at the same time. But perhaps this is a good enough start point.
I get 3274511360 when I run it. Although, I had to upgrade everything to uint because I was getting some overflow ... so there might be some of that in the output.
It punted on the request, which is to be fair what I also would do.
> Writing out an exact state of a multi-threaded program line-by-line is inherently challenging due to the inherent unpredictability of thread scheduling. However, I can provide an abstracted example of the flow of execution based on the order of operations in the code. Here is a simplified breakdown:
> x, y, z are instantiated as new X(), _staticCount is 0 for all, _memberCount is 0 for each individually.
> Create new Thread with lambda function and start it.
> In the new thread, Stuff(input - 1) recursively calls Stuff(3).
> This recursion continues until Stuff(0) is called, returning _staticCount + 1 which is 1.
> This recursion unwinds, incrementing _staticCount and _memberCount at each level.
> After the thread finishes, call Stuff(input - 1) again and add it to _memberCount, effectively doubling the _memberCount.
> Return the final value for Stuff(4) for x.
> Repeat the above steps for y.Stuff(3) and z.Stuff(2).
> Write the sum of resultX, resultY, and resultZ to the console.
> Please note that due to the nature of multi-threaded execution, there is a risk of race conditions. The Interlocked.Add method is used to help prevent these race conditions by making the increment operations atomic. Also, keep in mind that the state of _staticCount after all calls to the Stuff method will be dependent on the interleavings of the threads, and hence, not deterministic.
Interestingly enough, my threads here are deterministic because they always just wait for the execution to finish. So they're the same as not having them.
My plan was to go back and store off the threads generated and have something else wait for them, but I needed to think it through so that it could be done in a way to get deterministic results (like threads only read from member variables and writing to them is done in deterministic order or idempotently). Although, for a contrived example, it was more work than I really wanted to do.
I agree, punting is definitely the right course of action. I sort of have a vague intuition for what's going on here, but hammering down the exact details is just so tedious that I don't want to do it.
Although, it is interesting that the AI basically sees threads and blurts out that the problem is impossible, when they're not actually doing anything in this case.
It explained the code better than I would have, I think. “Inherently challenging” seems… accurate. I bet that if we’d used a data-parallel approach, e.g. with Rayon, it would have handled that better.
As a code assistant, it’s extremely useful if and only if you can check its work. And know how to prompt it.