TU ACM logo.
blog

Switch Statements Won't Fix Yandere Simulator

Many people have rightly criticized Yandere Simulator's overuse of if statements, but the issue is much more than switch statements can fix.
Posted 28 July 2020 at 1:30 PM
By Joseph Mellor

The code behind Yandere Simulator from r/ProgrammerHumor

Disclaimer

I am only going to talk about one commonly proposed solution to the excessive use of if statements in the code of Yandere Simulator and nothing else about its development or any controversy surrounding Yandere Simulator.

The Context

Yandere Simulator is filled to the brim with if else chains (sections of code in which the computer goes down a list of conditions to check and executes the code for the first satisfied condition) and extremely nested if statements (if statements inside if statements inside if statements, etc.), both of which are bad practice.

With the exception of one or two people I've seen on the internet, most criticisms that mention the excessive uses of if statements in Yandere Simulator follow the general idea that "long if else chains are a major performance problem and they should be replaced with switch statements to improve the performance." To be fair, some of these critics propose much better solutions than switch statements (specifically DarkDax and the Reddit post), but I will focus specifically on the idea that using switch statements will improve the performance significantly. For the same reason, I'll also ignore subjective arguments about style and readability.

The Code

In the code for the function UpdateRoutine(), the largest and most complicated function that will run for around a hundred students every frame in the largest file in the project, StudentScript.cs, there are around

Within UpdateRoutine(), there are many instances of code that look like

if (this.EatingSnack)
{
    if (this.SnackPhase == 0)
    {
        this.CharacterAnimation.CrossFade(this.EatChipsAnim);
        this.SmartPhone.SetActive(false);
        this.Pathfinding.canSearch = false;
        this.Pathfinding.canMove = false;
        this.SnackTimer += Time.deltaTime;
        if (this.SnackTimer > 10f)
        {
            UnityEngine.Object.Destroy(this.BagOfChips);
            if (this.StudentID != this.StudentManager.RivalID)
            {
                this.StudentManager.GetNearestFountain(this);
                this.Pathfinding.target = this.DrinkingFountain.DrinkPosition;
                this.CurrentDestination = this.DrinkingFountain.DrinkPosition;
                this.Pathfinding.canSearch = true;
                this.Pathfinding.canMove = true;
                this.SnackTimer = 0f;
            }
            this.SnackPhase++;
        }
    }
    else if (this.SnackPhase == 1)
    {
        this.CharacterAnimation.CrossFade(this.WalkAnim);
        if (this.Persona == PersonaType.PhoneAddict && !this.Phoneless)
        {
            this.SmartPhone.SetActive(true);
        }
        if (this.DistanceToDestination < 1f)
        {
            this.SmartPhone.SetActive(false);
            this.Pathfinding.canSearch = false;
            this.Pathfinding.canMove = false;
            this.SnackPhase++;
        }
    }
    else if (this.SnackPhase == 2)
    {
        this.CharacterAnimation.cullingType =
AnimationCullingType.AlwaysAnimate;
        this.CharacterAnimation.CrossFade(this.DrinkFountainAnim);
        this.MoveTowardsTarget(this.DrinkingFountain.DrinkPosition.position);
        base.transform.rotation = Quaternion.Slerp(base.transform.rotation,
this.DrinkingFountain.DrinkPosition.rotation, 10f * Time.deltaTime);
        if (this.CharacterAnimation[this.DrinkFountainAnim].time >=
this.CharacterAnimation[this.DrinkFountainAnim].length)
        {
            this.CharacterAnimation.cullingType =
AnimationCullingType.BasedOnRenderers;
            this.DrinkingFountain.Occupied = false;
            this.EquipCleaningItems();
            this.EatingSnack = false;
            this.Private = false;
            this.Routine = true;
            this.StudentManager.UpdateMe(this.StudentID);
            this.CurrentDestination = this.Destinations[this.Phase];
            this.Pathfinding.target = this.Destinations[this.Phase];
        }
        else if (this.CharacterAnimation[this.DrinkFountainAnim].time > 0.5f &&
this.CharacterAnimation[this.DrinkFountainAnim].time < 1.5f)
        {
            this.DrinkingFountain.WaterStream.Play();
        }
    }
}

With just the if, else, and else if statements, you get a structure that looks like

if (this.EatingSnack)
{
    if (this.SnackPhase == 0)
    {
        // Code
        if (/*time in range*/)
        {
            // Code
            if (/*check if student is rival*/)
            {
                // Code
            }
            // Code
        }
    }
    else if (this.SnackPhase == 1)
    {
        // Code
        if (/*Check details of current object*/)
        {
            // Code
        }
        if (/*Check distance in range*/)
        {
            // Code
        }
    }
    else if (this.SnackPhase == 2)
    {
        // Code
        if (/*time in range*/)
        {
            // Code
        }
        else if (/*time in range*/)
        {
            // Code
        }
    }
}

The Proposed Solution: Switch Statements

A problem with the above code might jump out at you: if this.SnackPhase is 2, then it can't also equal 0 or 1, but you have to check all of them. You might be thinking it may make more sense to get the value of this.SnackPhase and then execute the code that corresponds to the case where this.SnackPhase is 2. You can do using the titular switch statement, like so

if (this.EatingSnack)
{
    switch (this.SnackPhase)
    {
    case 0:
        // Code
        if (/*time in range*/)
        {
            // Code
            if (/*check if student is rival*/)
            {
                // Code
            }
            // Code
        }
        break;
    case 1:
        // Code
        if (/*Check details of current object*/)
        {
            // Code
        }
        if (/*Check distance in range*/)
        {
            // Code
        }
        break;
    case 2:
        // Code
        if (/*time in range*/)
        {
            // Code
        }
        else if (/*time in range*/)
        {
            // Code
        }
        break;
    default:
        break;
    }
}

A switch statement will tell the computer to map its input to relevant locations (memory addresses of specific instructions) in the code through a jump table. For example, the computer sees this.SnackPhase is 2, looks at the third entry in the jump table (0 is the first entry), and moves to the specified location (i.e. where the case 2: line is, though I'm simplifying slightly).

In this specific case, you might not see much benefit because switch statements have to do a little more work up front to do what they need to do, but they will execute within a constant amount of time. On the other hand if else chains have little start up time (an instruction to compare and an instruction to branch per if statement), but the amount of time they take to run grows linearly with the number of cases (double the cases means around double the time).

How Much Faster are Switch Statements?

As far as I can tell, none of the people who said to use switch statements instead of if else chains tested how much faster switch statements are, so I took it upon myself to get a decent estimate.

I wanted to see how much performance I could get out of a switch statement vs an if else chain, so I wrote some C code (I already have C set up on my computer and compilers don't need to do much to convert if and switch statements to optimal machine code, so it should be a good estimate) that ran a large number of iterations in which the program would pick a random number from a list of 16 specified numbers (partly to avoid the if statement using branch prediction), where the code looked like

int array[] = { 1, 3, 5, 6, 8, 12, 23, 30, 44, 56, 78, 88, 94, 97, 98, 99 };
int dummy_variable = 0;
// start timer
for (unsigned long long i = 0; i < num_iterations; i++) {
    value = array[rand() & 0b1111];
    if (value == 1) {
        dummy_variable = 1;
    } else if (value == 3) {
        dummy_variable = 3;
    }
...
    } else if (value == 99) {
        dummy_variable = 99;
    }
}
// end timer
// Do something with dummy_variable so it doesn't optimize the for loop out
// entirely
// start timer
for (unsigned long long i = 0; i < num_iterations; i++) {
    value = array[rand() & 0b1111];
    switch (value) {
    case 1:
        dummy_variable = 1;
        break;
    case 3:
        dummy_variable = 3;
        break;
    case 5:
...
    case 99:
        dummy_variable = 99;
    default:
        break;
    }
}
// end timer
// Do something with dummy_variable so it doesn't optimize the for loop out
// entirely
// start timer
for (unsigned long long i = 0; i < num_iterations; i++) {
    dummy_variable = rand() & 0b1111;
}
// end timer
// Do something with dummy_variable so it doesn't optimize the for loop out
// entirely

I also added another timed loop that just ran the random number generator in a loop so that I could subtract out the cost of looping and random number generation. After running the test, I found that the switch statement consistently ran around 20 times faster.

The Problem with Switch Statements

The if else chain took an average of around 24.5 nanoseconds to execute per iteration and around 8 if statements are hit on average per iteration (it's equivalent to a linear search so each individual if statement took around 3 nanoseconds per case while the switch statement took around 1.25 nanoseconds for any number of cases. For a game to run at 120 FPS, it has to do everything it needs to do for the next frame within 8.3 milliseconds. For if statements to take up a significant amount of that time (say 5%), over 130,000 if statements would have to be executed on at least one thread every frame.o

As I said earlier, UpdateRoutine() has around 1000 if statements and there are around 100 students, so you might be thinking that we've accounted for around 100,000 if statements. This reasoning fails to consider that the computer will execute relatively few if statements in the function. For example, if we look back at the sample code from UpdateRoutine(), you should notice that the inner if statements won't execute unless this.EatingSnack is true. If students are only eating a snack 5% of the time, we've removed around 1,000 if statements per frame.

Assuming each if and else if statement has a 50% of being true (a pretty good estimate for an upper bound since I don't have any prior knowledge of the probability and individual if statements in an if else chain have a lower chance of being true), I get around 63 if and else if statements evaluated on average for the entire function, meaning the computer will only evaluate 6,300 if statements per frame in UpdateRoutine() for all the students. Given that UpdateRoutine() likely has the most if statements out of all the functions that execute frequently and it is guaranteed to run for around 100 different entities, I doubt most other frequently executing functions would even come close to the upper bound 6,300 if statements of UpdateRoutine(), meaning that replacing all the if statements with switch statements wouldn't come close to netting a 5% performance boost even at 120 FPS. At the current ~20-50 FPS Yandere Simulator runs at, it wouldn't even increase the FPS by a single frame.

But switch Statements Make Your Code Fast!

If your program was a long chain of if else statements in a loop, then sure, but most of your program's time isn't going to be in evaluating if statements. If you want to make your code run fast, you need to optimize the slowest parts of your program first. As an example, if you have one function that takes up 1 second and another function that takes up an hour, a 50% speedup in the first task will only save you half a second but a 50% speedup in the second task will save you half an hour.

I would honestly go as far as to assume that using switch statements for performance is a premature optimization unless you could prove that you would get significant performance benefits, and even then you can replace them with an array or a map/dictionary most of the time. In my switch vs if else test program, I had another test where I get the value directly from an array, like so

// start timer
for (unsigned long long i = 0; i < num_iterations; i++) {
    dummy_variable = array[rand() & 0b1111];
}
// end timer
// Do something with dummy_variable so it doesn't optimize the for loop out
// entirely

The above code saves me 50 lines of code and the direct array access runs around 2.5x faster than the switch statement.

If you need more proof, YouTuber dyc3 profiled the code (and did an entire code review with deeper analysis and suggestions about coding architecture) and proved that the entire StudentScript.Update() function (which includes the function we looked at, UpdateRoutine() and every other update function) took less than a millisecond, which would be around 5% of the runtime at 50 FPS. Rendering poorly optimized assets took far more time than anything else, with poorly optimized physics, pathfinding, and UI interactions. dyc3 didn't stop there, however. He went through and replaced as many of the of the if else chains as possible with switch statements and only decreased the time by 80 microseconds. The game would have to be running at around 600 FPS for that improvement to be significant (above 5%).

The Real Problem

To be clear, the overuse of if statements is a major problem for maintainability and architecture (specifically the unnecessary coupling of data and code), not necessarily performance. For example, let's analyze a top competitor for the most infamous section of code in Yandere Simulator, the main body of SubtitleType TaskLineResponseType() in StudentScript.cs.

if (this.StudentID == 6)
{
    if (!false)
    {
        return SubtitleType.TaskGenericLine;
    }
    return SubtitleType.Task6Line;
}
else
{
    if (this.StudentID == 8)
    {
        return SubtitleType.Task8Line;
    }
    if (this.StudentID == 11)
    {
        return SubtitleType.Task11Line;
    }
    if (this.StudentID == 13)
    {
        return SubtitleType.Task13Line;
    }
    if (this.StudentID == 14)
    {
        return SubtitleType.Task14Line;
    }
    if (this.StudentID == 15)
    {
        return SubtitleType.Task15Line;
    }
    if (this.StudentID == 25)
    {
        return SubtitleType.Task25Line;
    }
    if (this.StudentID == 28)
    {
        return SubtitleType.Task28Line;
    }
    if (this.StudentID == 30)
    {
        return SubtitleType.Task30Line;
    }
    if (this.StudentID == 36)
    {
        return SubtitleType.Task36Line;
    }
    if (this.StudentID == 37)
    {
        return SubtitleType.Task37Line;
    }
    if (this.StudentID == 38)
    {
        return SubtitleType.Task38Line;
    }
    if (this.StudentID == 52)
    {
        return SubtitleType.Task52Line;
    }
    if (this.StudentID == 76)
    {
        return SubtitleType.Task76Line;
    }
    if (this.StudentID == 77)
    {
        return SubtitleType.Task77Line;
    }
    if (this.StudentID == 78)
    {
        return SubtitleType.Task78Line;
    }
    if (this.StudentID == 79)
    {
        return SubtitleType.Task79Line;
    }
    if (this.StudentID == 80)
    {
        return SubtitleType.Task80Line;
    }
    if (this.StudentID == 81)
    {
        return SubtitleType.Task81Line;
    }
    return SubtitleType.TaskGenericLine;
}

Remember that the long chain of if statements is equivalent to a chain of if else statements since the return statements will exit out as soon as one of the if statements are satisfied.

It would be best to separate our analysis into different levels that take progressively more information into account.

First-Level Analysis

Look for the specific feature of the language that would make this code faster without considering anything else. In this case, replace the if else chain with a switch statement, netting you a few microseconds (making this replacement a literal micro-optimization) for maybe 30 seconds of your time. People who have suggested using switch statements without first proposing architectural issues have gotten stuck here.

Second-Level Analysis

Look for the specific feature of the language that would make this code faster without considering anything else. In this case, replace the if else chain with a switch statement, netting you a few microseconds (making this replacement a literal micro-optimization) for maybe 30 seconds of your time.

Third-Level Analysis

Add the intended goal of this code into your analysis. Since you just want each student to have the proper SubtitleType, get the rid of the IDs entirely and create a class called Student with the field task_line with the default value SubtitleType.TaskGenericLine. When you want to know the SubtitleType for the student, ask the student with student.getSubtitleType(). Doing so removes unnecessary coupling between the task_line data for all students and some external function and gets rid of any operation except fetching a value from a known memory address. Alternatively, you could also use the fact that enums can be converted to integers and vice versa to make this entire function unnecessary.

Fourth-Level Analysis

Add the work of other people into your analysis. Third-level analysis is sufficient for TaskLineResponseType(), but the code of Yandere Simulator needs a small amount of fourth-level analysis, which would get rid of most of the if statements and clean up the code quickly and easily. Specifically, he's implementing a Finite State Machine, which have been implemented countless times in almost every language (all three links are in C# or work with C# and have existed for years) and explained in multiple tutorials. Unlike these other FSMs, which generally use dictionaries/arrays that map states to functions and other states, the FSM in Yandere Simulator is built implicitly with if statements, which can easily become unmaintainable as it loses any sense of regularity and can often lead to massive amounts of code duplication.

Summary

A picture of Joseph Mellor, the author.

Joseph Mellor is a Senior at TU majoring in Physics, Computer Science, and Math. He is also the chief editor of the website and the author of the tumd markdown compiler. If you want to see more of his work, check out his github repository.
Credit to Allison Pennybaker for the picture.