Showing posts with label optimization. Show all posts
Showing posts with label optimization. Show all posts

Thursday, April 15, 2021

Queen's Challenge - Part 4 of 6

 Part 4 – One Queen Per Row

Last time we saw a big problem with the brute force approach which we saw that queens were often placed on the same horizontal line as other queens which we know is not possible. This is a problem that can easily be rectified by imposing a constraint on where we are allowed to place the queens.  Ultimately, we are still going to be brute forcing the solution but by removing the most obvious issues with brute force.

The term constraint is simply mathematical speak about a rule restricting the values a variable can hold based on the value in other variables. Having more than one queen in a given row is clearly not valid so we can eliminate all boards where there are multiple queens on a single line. Applying the constraint that each queen must be on a unique row drastically will improve the performance of our search for a solution.

Creating our row-based brute force solver is simple enough as we can just extend the BaseSolver to get much of the logic. The only things we then need to implement is the reset and the next move methods and they are very similar to the brute force solver.

class RowBruteSolver extends BaseSolver {
   
constructor(game) {
       
super(game);
   
}

   
reset() {
       
this.locked = this.game.queens[0].y * 8 + this.game.queens[0].x;
        let
lockedRow = this.game.queens[0].y
       
let spot = 0;
        for
(let i = 0; i < 7; ++i) {
           
if (i === lockedRow)
                spot +=
8;
            this
.moveable[i] = spot;
           
spot += 8;
       
}
    }

 

As with the brute force solver, we set the locked tile to be the tile number of the tile based on the row and column of the locked tile in the game. We need to know which row this tile is on, which is just the y coordinate of the locked queen. As each row of tiles has 8 tiles on it, we know that the tile number will be 8*the row but we know we need to skip the row that row that the locked queen is on. To do this we use a spot variable that starts at 0 and if the current row is the same row as the locked row increases the spot by 8 to move the starting spot to the first (0) column of the next row. The queen is set and we update the spot for the next queen by adding 8 to it.

 

    nextMove() {
       
let pieceToMove = 0;
        for
(let i = 6; i >= 0; --i) {
           
let nextPos = this.moveable[i] + 1;
            if
((nextPos % 8) > 0) {
                pieceToMove = i
;
                this
.moveable[i] = nextPos;
                break;
           
}
        }
       
for (let i = pieceToMove+1; i < 7; ++i) {
           
this.moveable[i] = Math.floor(this.moveable[i] / 8)*8;
       
}
    }
}

 

The nextMove method tries to move the last row’s queen to the next column and if that move results in the queen going off the board , moves up the list row by row until it finds a queen that it can move to the right without going off the board. Once that has been done, we simply set the queens below it to the first tile of that column.

Watching the results of this solver clearly shows an improvement from the first solver. If you are patient and can wait a few hours, you can even see a solution to the game. Watching for a minute or so will quickly show us the next issue that we need to solve. We still are placing queens in locations were they are in conflict with each other and then processing all the queens below the conflicting queens even though we know that none of those potential layouts will result in a valid solution. Still this is a big improvement over the brute force approach.

From a worst-case perspective, we know that there are 7 queens with each queen having 8 possible locations so that would be 8^7 possible boards that need to be examined. While 2,097,152 boards is certainly better than 994,596,970,221 it is still quite a bit of work. We can do even better by finding a way to remove all those unnecessary searches. But how can we do this? A simple change to our solver will allow us to eliminate entire branches of moves so we don’t waste time testing moves that can’t possibly work as we will discover in the next part.

Wednesday, October 25, 2017

Thanks for the Memories

When I program, I try to follow the following pattern: get it working, get it working correctly, then if necessary get it running fast. Premature optimization is one of the bigger problems that programmers face. Often optimized code is hard to read and is the ideal spot for bugs to lurk. Making premature optimization even a worse habit is far too often you end up spending time optimizing the wrong code (not the thing that is actually causing the program to run slow) or are optimizing code that you are going to be replacing later. This is why optimizing after you have finished something makes the most sense.

After writing my C++ memory system for my emulator project, I realized that I really didn’t like the code. Several professors that I have had would call this code smell. The thing is, I really didn’t know why I didn’t like the code, just that it didn’t feel right. The subconscious is really good at determining when something isn’t right but feeds this information to the conscious mind in the form of vague feelings. I have learned that it is best to try and listen to these feelings and work out what your subconscious is trying to tell you.

My initial thoughts on the problem were that the code was not going to be efficient. With an emulator this could be a big concern as poor performance on memory operations would reduce the overall performance of the emulator. This lead me to thinking about other ways I could handle the memory and realized that I was prematurely optimizing the problem. This, however, may be a case where that is a good thing. The memory subsystem will be used by everything in the emulator so making sure the interface is locked down is important.

The big issue with the 2600 memory management is that I need to track reads and writes. If I only had to track writing, then memory could be a fast global array with only writes needing to be handled through a function. This got me researching the various 2600 bank switching schemes to verify if any need to handle switching on a read. The most common bank switching scheme does the switch on a LDA instruction so that approach will not work. As tools for refactoring code have improved immensely making such drastic changes to the code later may not be that big of a deal, so I decided to leave things alone and port the existing code to Kotlin.

While re-writing the code in Kotlin, I realized that I may be over-complicating things. In C++, the cartridge loader class would be passed to the A2600 class (the machine) which would then call the cartridge loader install code which would tell the A2600 which memory manager to use. The A2600 - specifically the TIA emulator and the 6502 emulator - would access memory by calling the MMU class and if the code resulted in a bank switch then the MMU would call the cartridge loader to adjust the banks. By having the memory accesses go through the cartridge and having the MMU built into the cartridge (it could still be a separate class but don’t think that is necessary at this point) things are much easier as this picture shows. This change should make any future optimization easier alieving me of most my conserns.



While I am now starting my disassembler, or at least writing the test for the disassembler, next week will be a postmortem of my Halloween game which will be released this weekend. It is a port of a really old “game” that I did  and even though it is pretty low on the polling results it is a cute game and would be easier to port now than later (more on why next week).

Thursday, May 21, 2015

The changing face of optimization

Optimization is simply the act of taking existing code and making it run more efficiently. This does not necessarily mean faster, as it is certainly possible to optimize code for storage size or for end user efficiency (work flow) or any number of other factors that management/clients deem important. For homebrew development, the key things that need to be optimized for are memory use and speed. This makes sense as the storage space on a cartridge is very limited. Likewise, frame rates are always important to maintain. One of the interesting things about doing homebrew development is it brings home how much optimization techniques have changed over the years.

The scariest change in optimizing is the “why don’t you just use the -O3 flag?” response that less experienced programmers give. I find this scary because it is an indication that compilers have become some type of magic tool to some people. Compiler optimization switches simply tell the compiler to take extra time to produce more efficient machine code. When you consider how lousy the code compilers produced use to be, this is very impressive. In the “old” days, your average assembly language programmer could write better code than the best compiler could produce. Now you need to be a really good assembly language programmer to produce better code making writing assembly language a mostly obsolete skill. Knowing how the machine works, however, will always be important so I do believe that all real programmers should know some assembly language even if they never use it.

It use to be possible to write faster code by employing techniques such as code re-ordering and loop unrolling. This is now done by the compiler when optimization is enabled. As code re-ordering and loop unrolling resulted in ugly and hard to understand code this is an improvement that I actually like.

The biggest change that has happened, and will probably continue to happen, is shifting bottlenecks. When I started programming, floating point was something you used as a last resort as is was so much slower than using integers. Integer techniques such as Fixed point math were used as despite the extra instructions and complexity they were still faster than floating point math. Now floating point is just as fast as integer math, and in certain cases may be faster.

Today’s biggest bottleneck is probably memory. We are now at a point in time where RAM access speeds are significantly slower than the speed that the processor is running at. I have heard it claimed that a cache miss will result in over a hundred cycle delay. This means that techniques such as lookup tables may be a lot slower than you would expect. In a few years this will probably change as technological solutions to the RAM bottleneck are discovered. At which point a new bottleneck will appear causing optimizers to have to change their techniques yet again.

Ultimately the best form of optimization is the same as it always has been. Simply to understand the problem enough that you can develop a better algorithm for solving it. This often requires thinking outside of the box which is something a compiler simply can’t do. When compilers get to the point that they are able to do this for the programmer, we should be very worried as the robot overlords will be soon to follow.