• No se han encontrado resultados

B

itvectors are an important part of optimizing small data items, yet they are so frequently missing from data structures books. Because they are fairly easy to understand, I have included them in this book. Bitvectors have many names, and you might have used something similar before, in which case you can skip this chapter. In this chapter, you will learn

What a bitvector is How to create a bitvector

How to access the bits inside a bitvector

How to rapidly set and clear every bit within a bitvector How to read and write a bitvector to disk

How to apply bitvectors to games using the quicksave method What a bitfield is

How to declare and access bitfields

What Is a Bitvector?

A bitvector is a specialized kind of array. Basically, a bitvector is meant to condense bit values (or booleans) into an array so that no space is wasted.

So why not just create a Boolean array? The reason is not so simple: Most compilers use a larger datatype, such as an integer, in place of a Boolean. They do this

because most computers can only send a fixed amount of bits at a time through memory and to the processor. Every x86 machine from the 386 upwards can only send data in packs of 32 bits.

Unfortunately, this is inefficient on a size basis. You often want data to take up the smallest amount of size possible, especially when you’re dealing with network trans- fers and saving massive virtual worlds to disk.

Enter the bitvector, designed to pack the data as closely as possible.

Designing a bitvector is a tricky task, however, because you need to use bit manipu- lation (see Appendix A if you are unfamiliar with bit manipulation). The method I use is to create an array of long integers, which are usually 32 bits long (see Figure

85

Graphical Demonstration: Bitvectors

support 32-bit integers, you can easily modify the bitvector class to work on larger or smaller integer sizes.

Now, for each index in the array, you should be able to access 32 individual bits.

Figure 4.1 bitvector containing 32 Here is a indexes. On most machines, these 32 indexes take up the same amount of room as a single integer.

Graphical Demonstration:

Bitvectors

The graphical demonstration for bitvectors is located on the CD that comes with this book in the directory \demonstrations\ch04\Demo01 - Bitvectors\ . This demon- stration shows you how to set and clear the bits within a bitvector. The other com- mon operations on a bitvector, such as resizing, creating, and deleting, are not shown in this demo because they are the same as the array algorithms that I have already demonstrated.

Compiling the Demo

This demonstration uses the SDLGUI library that I have developed for the book. For more information about this library, see Appendix B,“The Memory Layout of a Computer Program.”

To compile this demo, either open up the workspace file in the directory or create your own project using the settings described in Appendix B. If you cre- ate your own project, all of the files you need to include are in the directory.

The Main Screen

When you run the program, you are presented with the main screen, as shown in Figure 4.2. There are two buttons and a long bar containing white or grey boxes. This represents a bitvector that is two cells large, and each cell has 32 bits, giving you a total of 64 bits. White boxes mean that the cell has a value of 0, and grey boxes con- tain a value of 1. The boxes are somewhat small, but you can click on a box to select the current index, which is indicated with a red border around the box.

Figure 4.2

This is the main screen of the Bitvector Graphics Demonstration.

Using the Buttons

When you have selected the index that you want to set or clear, you must click on the Set Bit or Clear Bit buttons. After you do this, the demo goes through the pro- cedure to set or clear a bit. I cover the algorithm for these functions in the next section, and you can follow along using the demo.

Creating a Bitvector Class

In this section, I’m going to build a bitvector class. This class will assume that an

unsigned long int is 32 bits, which should work on the majority of systems out there. If not, it can easily be modified so that it works on integers of any bit size. The bitvector class is contained on the CD in the file \structures\bitvector.h.

87

Creating a Bitvector Class

The Data

First, I begin by creating the data members of the class:

class Bitvector {

protected:

unsigned long int* m_array; int m_size;

};

You’ll notice that the data members look almost exactly like the ones I used in the

Array class (see Chapter 3, “Arrays”), with the exception of the type of m_array. This time, it is an unsigned long int instead of a generic datatype, because a bitvector is only suitable for storing booleans.

The size variable keeps track of the number of integers within the array. Note that because I am using 32-bit integers to store the bitvector, the number of bits in the vector must be a multiple of 32 (32, 64, 96, and so on). Therefore, to find out how many bits are in the vector, you simply multiply the number of integers by 32. A 1- integer array can hold 32 bits, a 2-integer array can hold 64 bits, and so on.

The Constructor

The constructor for this class is the same as the array constructor:

Bitvector( int p_size ) {

m_array = 0; m_size = 0; Resize( p_size ); }

This piece of code clears the array pointer to 0, sets the size to 0, and then calls the

Resize algorithm to resize the array to the correct size.

The Destructor

Again, this part is the same as the array destructor:

~Bitvector() {

delete[] m_array; m_array = 0; }

This deletes the array if it exists.

The Resize Algorithm

The bitvector resize algorithm is similar to the array resize algorithm, with one exception: Instead of resizing the array to a certain number of integers, I perform a few calculations and resize the vector to the given number of bits. This change allows users of the class to request a certain number of bits without having to figure out how many integers they will turn into.

1: void Resize( int p_size ) 2: {

3: unsigned long int* newvector = 0;

4: if( p_size % 32 = = 0 ) 5: p_size = p_size / 32; 6: else

7: p_size = (p_size / 32) + 1;

8: newvector = new unsigned long int[p_size]; 9: if( newvector == 0 )

10: return; 11: int min;

12: if( p_size < m_size ) 13: min = p_size; 14: else

15: min = m_size; 16: int index;

17: for( index = 0; index < min; index++ ) 18: newvector[index] = m_array[index]; 19: m_size = p_size; 20: if( m_array != 0 ) 21: delete[] m_array; 22: m_array = newvector; 23: }

The only part of this algorithm that is different from the array resize algorithm is within lines 4–7. When a size is passed into this routine, it is assumed to be in bits, so I need to take that number and figure out how many cells to make. On line 4, I check to see if the number of bits is divisible by 32. If so, then the size of the array

89

Creating a Bitvector Class

is the number of bits required divided by 32. Hence, passing in 32 will result in 1 cell, 64 will result in two cells, and so on.

However, if the user passes in a number that is not divisible by 32, I need to do a lit- tle work. If the user passes in 31, for example, 31 divided by 32 will result in 0, because it is an integer division. 0 cells is obviously an incorrect amount, so I need to add 1 to the cell count, which is what happens on line 7. The end result of this algorithm is that you will always end up with a bitvector that contains as many bits as you need, plus some additional bits if the number isn’t divisible by 32.

If the integer you are using to store the bits isn’t 32 bits long, it is a simple task to change it. In lines 4–7, all you need to do is change all occurrences of 32 into the size of the integer you are using. If, for example, you are using an older 16-bit sys- tem, those four lines would look like this:

4: if( p_size % 16 == 0 ) 5: p_size = p_size / 16; 6: else

7: p_size = (p_size / 16) + 1;

The same goes for 8 bits, or 64 bits, or however many bits your integers use.

The Access Operator

This is one part of the bitvector class that deviates from the array class. In the array, I was able to make the access operator act in two ways: It could retrieve the value at an index and at the same time allow you to modify the item. You cannot do that with a bitvector.

The array access operator returned a reference to the item in the given cell, but because I am playing around with individual bits and not actual datatypes, I am not allowed to return a reference to a specific bit. So the access operator is limited to retrieving the value at a given index.

There are several parts to retrieving an individual bit within a bitvector: 1. Find the cell that the bit is in.

2. Find which bit in the cell is the required one. 3. Retrieve the bit.

4. Shift it down so it has a value of 0 or 1.

Step 1 is easy: To find out which cell a bit is in, divide the index by 32. If you want to find any bit from 0–31, it will be in the first cell; any bit from 32–63 will be in the second cell, and so on.

The next step is a little tricky. To figure out which bit in the cell you want to access, you need to take the original index and modulo it by 32. Any index from 0–31 modulo 32 will give you the same number, so if you want bit 5, you will need to retrieve the 5th bit of cell 0. What happens when you want to get bit 34? 34 modulo 32 is 2, so you access bit 2 of cell 1.

1: bool operator[] ( int p_index ) 2: {

3: int cell = p_index / 32; 4: int bit = p_index % 32;

5: return (m_array[cell] & (1 << bit)) >> bit; 6: }

Lines 3 and 4 find the cell and bit-index that you want to retrieve, which parallel steps 1 and 2 of the algorithm, but line 5 needs some explaining. First of all, you access the integer at index cell. This returns an integer. Next, you take 1 and shift it up bit spaces. Now, this should give you a 1 at the same bit position as the bit you want to retrieve, right? Take a look at Figure 4.3 to see how this works.

Figure 4.3

This shows the 1 being shifted up into the correct position.

If you want to retrieve bit 0, then 1 shifted up 0 places is still 1. If you want to access bit 5, then 1 shifted up 5 places is 32, which, represented in binary, is 100000. The 1 is in index 5.

Now that you have shifted a 1 into the appropriate place, you need to retrieve the bit in the cell. This step is easy—all you need to do is binary and the two numbers together. Remember the binary rules:

1 & 1 = 1

91

Creating a Bitvector Class

So when you take that 1 and binary and it with the given cell, you essentially retrieve the bit in the array at the correct bit-position. However, the result of the

binary and isn’t a 1 or a 0. If bit 5 had a 1 in it, then the result of the operation would be 32, or 100000. You need to shift this number back down so that it is either a 1 or a zero. So you shift it down 5 bits, and voila! You have a 1!

Note that you can modify the access algorithm for any size integer by replacing all occurrences of 32 with whatever integer size your platform uses.

The Set Function

Setting a bit within the bitvector is a slightly more complicated task. Because there is no single way to set an individual bit within an integer, you need to rely on the binary math rules: Use the and operator to clear bits and use the or operator to set bits.

1: void Set( int p_index, bool p_value ) 2: {

3: int cell = p_index / 32; 4: int bit = p_index % 32; 5: if( p_value == true )

6: m_array[cell] = (m_array[cell] | (1 << bit)); 7: else

8: m_array[cell] = (m_array[cell] & (~(1 << bit))); 9: }

Lines 3 and 4 are the same from the access operator; they retrieve the cell number and the bit number. At this point, you need to make a choice: If the value you want to set is true, then you want to set the correct bit within the vector; if the value you want to set is false, then you want to clear the correct bit within the vector. To do this, you rely on four binary math rules:

1. x and 1 = x

2. x and 0 = 0 3. x or 1 = 1 4. x or 0 = x

Rules 1 and 4 are known as identity functions, which just return the same value as x. Rule 2 is the clear function—no matter what x is, the result is 0. Rule 3 is the set

For the set function to work (line 6), you shift a 1 into the bit position that you want to set, and you logically or that with the correct cell. This process is demon- strated in Figure 4.4.

Figure 4.4

This shows how to set a bit. Note that every bit in the final result is the same except for the one bit that I wanted to set, which became 1.

Once you are done with the operation, the correct bit is set.

For the clear function to work, you need to do a little more work. This time, to clear the correct bit and keep all the other bits the same, the bit you want to clear needs to be 0, and every other bit needs to be 1. Remember, using the logical and opera- tor with a 1 is the identity function. Figure 4.5 demonstrates this algorithm.

Figure 4.5

This shows how to clear a bit. Note that every bit in the final result is the same except for the one bit that you wanted to clear, which became 0.

So you use the shift operator to first shift a 1 into the desired position that you want to clear. Then, the logical not operator reverses every bit so that there is a 0 where the 1 was, and everything else is now 1. After using the logical and operator on the cell, the desired bit is now cleared.

93

Creating a Bitvector Class

If you want to convert the algorithm to an integer size different from 32 bits, just change all instances of 32 to the desired bit size.

The ClearAll Function

There are times when you will want to clear the entire contents of a bitvector quickly, and as you might guess, looping through every bit and clearing it doesn’t seem to be efficient. Instead, there is a better method, where you set each integer in each cell to 0. On a 32-bit system, you’ve just set 32 bits to 0 at once.

1: void ClearAll() 2: {

3: int index;

4: for( index = 0; index < m_size; index++ ) 5: m_array[index] = 0;

6: }

The algorithm loops through and sets every cell to 0. Figure 4.6 shows how the clear function works.

Figure 4.6

This figure represents an 8-celled bitvector. If each cell had 32 bits, then there would be 256 bits.The algorithm sets each cell to 0, clearing 32 bits at a time.

The SetAll Function

There might also be times when you need to set every bit in the bitvector to 1. Luckily, the procedure is just as easy as the ClearAll function—instead of replacing each integer with 0, you replace each integer with a number that is all 1s. On a 32- bit system, this number would be hexadecimal FFFFFFFF, or decimal 4,294,967,295. Each F in the hex representation is 4 bits, so you need 8 of them for 32 bits.

1: void SetAll() 2: {

3: int index;

4: for( index = 0; index < m_size; index++ ) 5: m_array[index] = 0xFFFFFFFF;

6: }

If you were to use this algorithm for a different integer size, you would need to replace 0xFFFFFFFF with the hex equivalent for the correct size. 8 bits would be 0xFF, 16 bits would be 0xFFFF, and so on.

The WriteFile Function

Because a bitvector is an array of integers, saving a bitvector to disk is the same as the way you save arrays to disk.

1: bool WriteFile( const char* p_filename ) 2: {

3: FILE* outfile = 0; 4: int written = 0;

5: outfile = fopen( p_filename, “wb” ); 6: if( outfile == 0 )

7: return false;

8: written = fwrite( m_array, sizeof(unsigned long int), m_size, outfile );

9: fclose( outfile ); 10: if( written != m_size ) 11: return false;

12: return true; 13: }

The only change in the algorithm is on line 8—instead of passing the size of a generic datatype, you pass the size of an unsigned long integer.

The ReadFile Function

Like the WriteFile function, the ReadFile function is almost the same as the

95

Creating a Bitvector Class

1: bool ReadFile( const char* p_filename ) 2: {

3: FILE* infile = 0; 4: int read = 0;

5: infile = fopen( p_filename, “rb” ); 6: if( infile == 0 )

7: return false;

8: read = fread( m_array, sizeof(unsigned long int), m_size, infile );

9: fclose( infile ); 10: if( read != m_size ) 11: return false; 12: return true; 13: }

The only change is on line 8, where you change the generic datatype to an unsigned long int.

Example 4-1

This is Example 4-1, which can be found on the CD in the directory \examples\ch04\01 - The Bitvector Class\ .

Here is the code listing for Example 4-1, which covers all of the basic features of the Bitvector class:

void main() {

// create a bitvector with 32 bits. Bitvector bitv( 32 );

bool b;

// set index 0 to true and retrieve it again.

bitv.Set( 0, true );

b = bitv[0];

// set index 31 to false and retrieve it again.

bitv.Set( 31, false );

b = bitv[31];

// set all the bits in the vector to 0

// set all the bits in the vector to 1

bitv.SetAll();

// resize the bitvector to 48 bits

bitv.Resize( 48 );

// get the size of the bitvector.

int s = bitv.Size();

// Why is s = 64? Remember, because you are on a 32-bit system, // you can only have multiples of 32. Because you asked for 48 // bits, the resize algorithm had to make enough room for 48 bits, // so it jumped up to the next level and made 64.

}

This example has no output.

Application: The Quicksave

This is Game Demonstration 4-1, which can be found on the CD in the directory \demonstrations\ch04\Game01 - Saving Players\ .

Compiling the Demo

This demonstration uses the SDLHelpers library that I have developed for the book. For more information about this library, see Appendix B, “The Memory Layout of a Computer Program.”

To compile this demo, either open up the workspace file in the direc- tory or create your own project using the settings described in Appendix B. If you create your own project, all of the files you need to include are in the directory.

Games these days are huge. They take up hundreds of megabytes of memory at any given time and simulate many things all at once. Games are usually so large that

Documento similar