Open hgducharme opened 1 year ago
Prefer out-of-source builds for compiled projects. It's so much easier when using version control and keeping build artifacts separate from source files
Consider the directory tree
.
├── Makefile
├── bin
│ ├── project.out
│ └── tests.out
├── build
│ ├── src
│ │ ├── class1.o
│ │ └── main.o
│ └── test
│ └── test_class1.o
├── src
│ ├── class1.cpp
│ ├── class1.h
│ └── main.cpp
└── test
└── test_class1.cpp
Suppose:
main.cpp
includes class1.h
test_class1.cpp
includes class1.h
project.out
executableWorking backwards:
bin/project.out
executable, we have to link together the build/src/class1.o
and build/src/main.o
files.class1.o
file we have to compile the class1.cpp
filemain.o
file we have to compile the main.cpp
file# Compile class1.cpp
clang++ --compile src/class1.cpp --output build/src/class1.o
# Compile main.cpp
clang++ --compile src/main.cpp --output build/src/main.o
# Link them together to produce an executable
clang++ build/src/class1.o build/src/main.o --output bin/project.out
You can compile implicitly like
clang++ /src/main.cpp /src/class1.cpp --output /bin/project.out
but then you don't get .o files. Not quite sure the cons of this approach.
tests.out
executableWorking backwards:
bin/tests.out
executable, we have to link together the build/src/class1.o
and build/test/test_class1.o
files.class1.o
file we have to compile the class1.cpp
filetest_class1.o
file we have to compile the test_class1.cpp
file# Compile class1.cpp
clang++ --compile src/class1.cpp --output build/src/class1.o
# Compile test_class1.cpp
clang++ --compile test/test_class1.cpp --output build/test/test_class1.o
# Link them together to produce an executable
clang++ build/src/class1.o build/test/test_class1.o --output bin/tests.out
Helpful stackoverflow post for more
When dealing with binary data and bit patterns in C/C++, people will generally represent these with hexadecimal (e.g. "0xffff0000") or octal numbers. There's a direct mapping between hex/octal numbers and bit patterns that makes the code a bit more readable and understand, which isn't available when you use decimal values (e.g. "4571894638") to represent bit patterns.
Binary literals weren't around in C until C23 and C++ until C++14. We have access to binary literals now but this is why you will see hex/octal numbers floating around. To see how to use binary literals go here.
Tangentially related, a bitmask is just a value that is used with another value to perform certain operations on the bits of the second value. Think of it like a linear operator in linear algebra: you apply the linear operator A
to a vector x
in order to perform some operation on that matrix and get the resulting vector b
(Ax = b
).
See cppreference on binary arithmetic operators for best practices on overloading bitwise operators
If a function definition returns a reference like
X & function()
but you don't specify a reference when calling it, you will not get a reference to the original value. You will copy it maybe?
// Will store the value in a NEW value called var. Any modifications to var will not modify the original value
X var = function();
// Will obviously return a reference to the original value. Any modification to var will modify the original value
X & var = function();
In other words, run-time polymorphism vs compile-time polymorphism.
When to use an interface vs a template?
Some takeaways:
What is the performance penalty when using templates over an interface
I'm starting to question my use of my Bitboard
class that acts as a wrapper for the primitive type uint64_t
. I like the syntactic sugar it provides such as
Bitboard blackPawns(DEFAULT_BLACK_PAWN_STRUCTURE);
blackPawns.setBit(3);
blackPawns.numberOfSetBits();
blackPawns.findIndexLSB();
instead of
u64 blackPawns = DEFAULT_BLACK_PAWN_STRUCTURE)
setBit(blackPawns, 3);
numberOfSetBits(blackPawns);
findIndexLSB(blackPawns);
I'm concerned about future performance. I'm starting to see that I need to instantiate the class just to represent something as a uint64_t
and pass that to something else. I feel like this is unnecessarily using a lot of stack memory which will catch up to me later on by causing speed issues in the engine.
I'm wondering if I should spend the time now to get rid of the class and just use using Bitboard = std::uint64_t
, or if using the wrapper class should be fine moving forward.
Here are some good reads:
Bitboard::clearAndReturnMSB()
are made to be non-friend non-member functions, or at least friend non-member functionsAlthough this might be a bad design, I'm going to continue with it for now. Once i get a minimum viable product I can profile the code and see if this is a bottleneck. If so, we will optimize it later, but not now
Side note:
64 - n
bits, where n
is the number of bits in the resulting product i
.i
generated in step 3 to access a pre-initialized move databaseThe goal of a magic number is such that when it is multiplied against an occupancy mask, it maps the scattered placement of occupied bits into a consecutive ordering in the most significant bit positions. This function, which maps the relevant occupancy set to a range of attack-sets per square, is surjective. The equation is:
index = (occupancy set * magic number) >> (64 - number of bits in occupancy set)
For example, consider a bishop on B1:
any consecutive
relevant occupancy combination of
for bishop on b1, magic number the masked bits
5 bits
. . . . . . . . . . . . . . . . . . .[C D E F G]
. . . . . . . . . 1 . . . . . . . . . . . . . .
. . . . . . G . . 1 . . . . . . . . . . . . . .
. . . . . F . . . 1 . . . . . . . . . . . . . .
. . . . E . . . * . 1 . . . . . . = . . garbage . . >> (64- 5)
. . . D . . . . . 1 . . . . . . . . . . . . . .
. . C . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
Consider a rook on A1, then the relevant occupancy mask, or the potential blocker squares are:
0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0
0 1 1 1 1 1 1 0
Question: What is the performance difference between k
for loops all with simple instructions versus one for loop with all instructions inside it, and all for loops have the same amount of iteration loops?
Research
Consensus
Apparently k
multiples of a for loop can theoretically be faster than one for loop with a lot of complex instructions. Both situations are obviously O(n)
, but there might be slight optimizations that can be made. I suppose it really depends on the context and this is kind of considered a micro optimization, which shouldn't be done unless you profiled the code and have evidence that one is a bottleneck compared to the other.
Decision I personally sort of like the idea of multiple for loops with simple instructions in them because it allows me to write functions with good encapsulation and naming, and therefore aiding in readability and maintainability. I suppose I will go ahead with that route and can profile the code if this is slow and I need it to be faster.
Apparently you can't initialize C++ arrays (in shorthand) with a value other than zero. That is, the following would not behave as you expect
int myArray[10][10] = { {50} };
The first element would be 50 but all other elements would be zero. You can use std::fill
to set all elements to 50. For a concise answer on why, see this stackoverflow answer.
Cool video on psuedo random number generation in c++ and why rand()
is harmful
constexpr
functions require a return type that is considered a literal type.
https://stackoverflow.com/questions/34801622/difference-between-benchmarking-and-profiling
Static
keywordOn class member functions and variables: defining a class member function or variable as static
means it belongs to the class and not any particular instance of the class
On non-member functions and variables: defining a non-member function or variable as static
means it only has internal linkage and will not be used outside of its translation unit. This use of static
has been replaced by anonymous namespaces, but they both do exactly the same thing. The benefit of using static
is that it's clear by function definition alone that it has internal linkage, and there's no need to scroll up or down to see if it's contained in an anonymous namespace.
Have learned a lot about docker implementing this CI pipeline. There's some super niche quirks when integrating with Github Actions.
One thing that was tripping me up was that I was defining a non-root user in my docker containers but this doesn't work on Github Action servers, as detailed in this article.
Ok wow just had to detangle the biggest mess with an unnamed namespace nested within a named namespace and split between the interface file .h
and the implementation file .cpp
What I had before:
// .h
namespace xyz {
void a();
namespace {
void b();
void c();
}
}
// .cpp
namespace xyz {
void a() { b(); }
namespace {
void b() { c(); }
void c() { ... }
}
}
Technically this will compile and work just fine but you will get the error in the header file for b()
saying it was declared static but never defined, and therefore it's an unused function.
Ultimately you're not supposed to put anonymous namespaces in header files, so I went ahead and removed the anonymous namespace from the header file, but then in the source file you will get the error that b()
is not defined in the scope of a()
, and therefore a()
can't call b()
. The issue here is that now our function definitions simultaneously become declarations also, and since c++ presumably parses the file from the top down, then it hasn't seen the declaration for b()
when it parses a()
. Therefore, you need to move the anonymous namespace to the top of the named namespace. However, you will still have the function ordering problem within the unnamed namespace. You must also keep the unnamed namespace inside the named namespace for a()
to see b()
.
TLDR: The following is what works
// .h
namespace xyz {
void a();
}
// .cpp
namespace xyz {
namespace {
void c() { ... }
void b() { c(); }
}
void a() { b(); }
}
Cool discussions on array of structs vs array of pointers to structs:
Ran into this question when considering how I should manage BISHOP_HASHING_PARAMETERS_LOOKUP
and ROOK_HASHING_PARAMETERS_LOOKUP
. Right now they are arrays of structs, and I will just keep it like that until I profile the code and see where bottlenecks are
class Abstract
{
Abstract();
~Abstract();
void functionWithADefinition { does stuff... };
virtual void virtualFunctionWithDefinition { does stuff... };
virtual void pureVirtual() = 0;
// Abstract classes can contain member variables
int memberVariable = 0;
}
class Interface
{
virtual ~Interface() = 0; // or virtual ~Interface() {};
virtual void pureVirtual1() = 0;
virtual int pureVirtual2() = 0;
}
Both abstract classes and interfaces define the concept of an interface (or API) to clients that use them, the only difference is that abstract classes have pre-packaged functionality that is shared between implementations. Use an interface if there is no shared functionality. Use an abstract class if there isn't one.
All interfaces (pure abstract classes) are abstract classes, but not all abstract classes are interfaces. Before using either, make sure the implementations has an is-a relationship with the parent class.
Consider the abstract class Animal
with two implementations Dog
and Cat
. To instantiate an implementation but specify the base type Animal
, you must dynamically initialize it (as opposed to statically).
int main()
{
// Static initialization with automatic storage duration; this will not work
Animal dog();
Animal dog = Dog();
// Dynamic initialization with dynamic storage duration; this will work
Animal * dog = new Dog();
delete dog;
return 0;
}
To use a base class as a parameter type to a function, the object must by reference or pointer
void function(Base * derivedObject); // works
void function(Base & derivedObject); // works
void function(Base derivedObject); // will not work
The cleanest way to only use the interface type in client code seems to be by using a factory, as shown in this answer
// position_evaluator.h
namespace interfaces { class PositionEvaluator { ... }; }
enum class EvaluatorImplementation { NeuralNetwork, Minimax };
class NeuralNetwork : public interfaces::PositionEvaluator { ... };
class Minimax : public interfaces::PositionEvaluator { ... };
interface::PositionEvaluator * createPositionEvaluator(EvaluatorImplementation implementation) { ... };
// search_algorithm.h
namespace interfaces { class SearchAlgorithm { ... }; }
enum class SearchImplementation { MonteCarloTreeSearch, MonteCarloGraphSearch};
class MonteCarloTreeSearch : public interfaces::SearchAlgorithm { ... };
class MonteCarloGraphSearch : public interfaces::SearchAlgorithm { ... };
// A search algorithm requires a PositionEvaluator & to be passed to the constructor
// Technically a factory should abstract away all details about creating dependencies for the object it's going to create,
// but it's not horrendous to pass in a dependency
interface::SearchAlgorithm * createSearchAlgorithm(SearchImplementation implementation, PositionEvaluator & evaluator) { ... };
// main.cpp
#include "search_algorithm.h"
#include "position_evaluator.h"
int main()
{
// These could probably even return references
// If using pointers, the most responsible thing to do would be to return an std::unique_pointer<PositionEvaluator> and std::unique_pointer<SearchAlgorithm>
PositionEvaluator * neuralNetwork = createPositionEvaluator(EvaluatorImplementation::NeuralNetwork);
SearchAlgorithm * monteCarloTreeSearch = createSearchAlgorithm(Implementations::MonteCarloTreeSearch, (*neuralNetwork) );
return 0;
}
But this has gotten absolutely insane for something that's known at compile time. It does massively improve readability and modularity, however
When this is ultimately turned into a technical presentation, the lessons learned should be formatted by categories:
CPPFLAGS
are flags that are passed to the C PreProcessorCXXFLAGS
are flags that are passed to the C++ compiler