Page 1 of 1

performance of bitwise operators vs conventional methods

Posted: Thu Aug 08, 2024 10:13 pm
by Noiecity
I was studying about C++14, and I came across "bitwise operators", I read that it offered better performance in applications where low latency is critical, so I started to compare. At first there was no performance advantage when comparing on a single loop, this is because comparing on a single loop can be affected by the speed and precision of the timer, as well as initializing the test right after running the program.

To my surprise, once I made the program wait two seconds before starting each test, and then benchmark 10 cycles instead of one, the difference became noticeable.

I obtained between 1 to 2 seconds better performance in terms of latency with bitwise operators.

Code: Select all

#include <iostream>
#include <chrono>
#include <vector>
#include <string>
#include <thread>

// Define object states using bitwise operators
const int STATE_ACTIVE = 1 << 0; // 0001
const int STATE_INVISIBLE = 1 << 1; // 0010
const int PLAYER_COLLISION = 1 << 2; // 0100
const int ENEMY_COLLISION = 1 << 3; // 1000

// Conventional structure
struct ObjectState {
	bool active;
	bool invisible;
	bool playerCollision;
	bool enemyCollision;
};

// Function to measure execution time and memory usage
template <typename Func>
void measurePerformance(Func func, const std::string& name, int cycles) {
	// Prepare the CPU
	std::this_thread::sleep_for(std::chrono::seconds(2));

	auto start = std::chrono::high_resolution_clock::now();
	for (int i = 0; i < cycles; ++i) {
		func();
	}
	auto end = std::chrono::high_resolution_clock::now();
	std::chrono::duration<double> duration = end - start;

	std::cout << "Execution time of " << name << " for " << cycles << " cycles: " << duration.count() << " seconds" << std::endl;
}

// Function using bitwise operators
void bitwiseOperators() {
	std::vector<int> objects(1000000, STATE_ACTIVE | STATE_INVISIBLE | ENEMY_COLLISION);

	for (int state : objects) {
		if (state & STATE_ACTIVE) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				int result = state & (1 << i);
			}
		}
		if (state & STATE_INVISIBLE) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				int result = state & (1 << i);
			}
		}
		if (state & PLAYER_COLLISION) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				int result = state & (1 << i);
			}
		}
		if (state & ENEMY_COLLISION) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				int result = state & (1 << i);
			}
		}
	}
}

// Function using conventional methods
void conventionalMethods() {
	std::vector<ObjectState> objects(1000000, { true, true, false, true });

	for (const ObjectState& state : objects) {
		if (state.active) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				bool result = state.active && (i % 2 == 0);
			}
		}
		if (state.invisible) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				bool result = state.invisible && (i % 2 == 0);
			}
		}
		if (state.playerCollision) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				bool result = state.playerCollision && (i % 2 == 0);
			}
		}
		if (state.enemyCollision) {
			// Perform more complex operations
			for (int i = 0; i < 10; ++i) {
				// Simulation of complex operations
				bool result = state.enemyCollision && (i % 2 == 0);
			}
		}
	}
}

int main() {
	int cycles = 10;

	// Measure the performance of bitwise operators
	measurePerformance(bitwiseOperators, "bitwise operators", cycles);

	// Measure the performance of conventional methods
	measurePerformance(conventionalMethods, "conventional methods", cycles);

	// Measure memory usage
	std::cout << "Size of an object with bitwise operators: " << sizeof(int) << " bytes" << std::endl;
	std::cout << "Size of an object with conventional methods: " << sizeof(ObjectState) << " bytes" << std::endl;

	system("pause");
	return 0;
}
Result:

Image

Re: performance of bitwise operators vs conventional methods

Posted: Thu Aug 08, 2024 11:59 pm
by CuteAlien
Slight risk the compiler will optimize away your "complex operation" as the result isn't used. Better to have one result variable on top and maybe print out at the end or something so it can't be removed. If it's not removed I'd rather suspect it's not compiled with optimizations.
Also not sure if using a const ref in one loop and copy in the other is a good idea, as they have same size I'd make the loop as similar as possible.

And often good idea for speed comparisons to run the test twice and switch the order for second run (I did that when I just run it myself). I didn't rewrite code to avoid optimization away inner loop (which may have happened... but I gotta stop for today).

My results for this in debug:
Execution time of bitwise operators for 10 cycles: 0.312858 seconds
Execution time of conventional methods for 10 cycles: 0.357684 seconds
Execution time of conventional methods for 10 cycles: 0.359197 seconds
Execution time of bitwise operators for 10 cycles: 0.31793 seconds
Size of an object with bitwise operators: 4 bytes
Size of an object with conventional methods: 4 bytes

But in release (with -O3):
Execution time of bitwise operators for 10 cycles: 0.00573853 seconds
Execution time of conventional methods for 10 cycles: 0.00532468 seconds
Execution time of conventional methods for 10 cycles: 0.00491474 seconds
Execution time of bitwise operators for 10 cycles: 0.00530345 seconds
Size of an object with bitwise operators: 4 bytes
Size of an object with conventional methods: 4 bytes

edit: There might be more stuff, but in general the main reason I'd expect bit operation to be faster is that in general we don't pack bools in bits (which is also possible in some cases) and so more information can be put together. And that can help with caching. Which is not case here obviously (as same size).

Re: performance of bitwise operators vs conventional methods

Posted: Fri Aug 09, 2024 12:36 am
by Noiecity
CuteAlien wrote: Thu Aug 08, 2024 11:59 pm Slight risk the compiler will optimize away your "complex operation" as the result isn't used. Better to have one result variable on top and maybe print out at the end or something so it can't be removed. If it's not removed I'd rather suspect it's not compiled with optimizations.
Also not sure if using a const ref in one loop and copy in the other is a good idea, as they have same size I'd make the loop as similar as possible.

And often good idea for speed comparisons to run the test twice and switch the order for second run (I did that when I just run it myself). I didn't rewrite code to avoid optimization away inner loop (which may have happened... but I gotta stop for today).

My results for this in debug:
Execution time of bitwise operators for 10 cycles: 0.312858 seconds
Execution time of conventional methods for 10 cycles: 0.357684 seconds
Execution time of conventional methods for 10 cycles: 0.359197 seconds
Execution time of bitwise operators for 10 cycles: 0.31793 seconds
Size of an object with bitwise operators: 4 bytes
Size of an object with conventional methods: 4 bytes

But in release (with -O3):
Execution time of bitwise operators for 10 cycles: 0.00573853 seconds
Execution time of conventional methods for 10 cycles: 0.00532468 seconds
Execution time of conventional methods for 10 cycles: 0.00491474 seconds
Execution time of bitwise operators for 10 cycles: 0.00530345 seconds
Size of an object with bitwise operators: 4 bytes
Size of an object with conventional methods: 4 bytes
Interesting, thanks for sharing information, I am still very new to c++, so I appreciate the suggestions, I have never compiled using -O3, I usually just use visual studio, but at least now I know that visual studio can optimize code haha(/Ob2, /Ot, /Ox, /GL)

Re: performance of bitwise operators vs conventional methods

Posted: Fri Aug 09, 2024 9:55 am
by CuteAlien
Ah yes, -O3 is with g++ compiler. Visual studio tends to automatically create 2 compile targets called "debug" and "release" for new c++ projects. And release has all the typical optimizations. In general people work with debug for development (because debugging is easier) and release for releasing it to other people (as it's _way_ better optimized). Thought once in a while you also have to test release builds in development (for example some evil bugs tend to not show up in debug).

Re: performance of bitwise operators vs conventional methods

Posted: Fri Aug 09, 2024 3:41 pm
by Noiecity
CuteAlien wrote: Fri Aug 09, 2024 9:55 am Ah yes, -O3 is with g++ compiler. Visual studio tends to automatically create 2 compile targets called "debug" and "release" for new c++ projects. And release has all the typical optimizations. In general people work with debug for development (because debugging is easier) and release for releasing it to other people (as it's _way_ better optimized). Thought once in a while you also have to test release builds in development (for example some evil bugs tend to not show up in debug).
it seems to me that modern c++ compilers are so well optimized, that it must even be difficult to make assembly-level optimizations and see a real improvement in performance, since the compiler has probably already made the improvement at the assembly level.

edit: literally, some time ago I tried to write a program in assembly, and I compared it with c++ to see "how much faster assembly was compared to c++", ironically c++ performed better in the tests, the c++ compiler produced better assembly code than I did