Sunday, October 24, 2010

Creating a Super-Fast Math Library, Part 2: Basics

Before we get to digging into the fun information, I thought it'd be helpful if I described what types I use and a little about my coding style.

A good portion of what I write is in C, but I use C++ namespaces, templates, and classes where appropriate. I always prefer clarity over brevity. Saving space by putting a lot of data on one line makes the code unreadable by everyone but the author (actually, it's unreadable to them as well, they just don't admit it.) Also, there is no such thing as "self documenting code." If I have to mentally trace through the code to understand what it's doing, it needs documentation.

In my engine I have various typedefs set up for standard types. This is one thing that allows me to easily switch platforms and not have to worry about, for instance, the size of an int versus the size of a long.

It's a pretty standard practice, but here's the basic types that I use:

typedef unsigned char      uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

typedef signed char sint8_t;
typedef signed short sint16_t;
typedef signed int sint32_t;
typedef signed long long sint64_t;

typedef float f32_t;
typedef double f64_t;


There are actually more than these and they are different for different platforms, but this gives you an idea of what they are when I use them later.

After that, I have a couple of macros that align data. Because the Playstation3 uses alignment attributes that follow the data declaration, we have to either double up with pre and post macros or declare a special DECLARE_MY_ALIGNED_VARIABLE(int thing) which I'm not overly fond of. Who knows, maybe one day I'll change my mind.

# define PRE_ALIGN(x) __declspec(align(x))
# define POST_ALIGN(x)

Another thing that you may (or may not...) see is branch prediction hints. MSVC++ optimizes code under the assumption that it will always enter the first case of a conditional. For example:


if (a) // this is predicted as always being true
{
do_a_stuff(); // which (hopefully...) loads this code in the cache
}
else
{
do_not_a_stuff(); // but doesn't load this code.
}


When a branch prediction is wrong, the CPU has to grab the code and load it into memory, which takes time. Data and code cache misses kill a program's speed like an army of ants descending on a picnic.

My branch prediction hints are simply defined as this for Windows:

# define LIKELY(x) x
# define UNLIKELY(x) x

In other words... they don't do anything. But, if you see them, you know what they're for.

On a final note about coding conventions, style, and so forth... I tend to adapt to what I need, not pick a particular book style and strictly adhere to it. If I see something I like that suits my needs I'll probably end up using it. I'm always open to new ideas.

Creating a Super-Fast Math Library, Part 1

This is part one of an open ended series on writing a super fast math library using SIMD. The goal of the project was to write a math library that is both cross platform and very, very fast.

I've had a lot of fun doing it and have learned quite a bit and thought I'd share what I've done so far.

Let's skip the small talk and get down to business. The goal is to write vector, matrix and quaternion types that are 1) correct, 2) cross platform, and 3) fast. All of the development has, thus far, been done using Microsoft Visual Studio 5 under Windows.

To get timing results, I'm using Agner Fog's Test programs for measuring clock cycles and performance monitoring. It uses the rdtsc instruction to determine clock counts, cache misses, branch mispredictions and a host of other stuff.

For intrinsics, I am strictly limiting myself (at the moment) to SSE version 1. It is supported on Intel, AMD, and VIA chipsets going back quite a bit.

The platforms I'm targeting are the Xbox, Playstation3, and, of course, the PC but all of the information I'm putting here is PC oriented. The platforms are vastly different and, while I'd love to cover all three in deep meaningful investigation... I really don't have the time. :)

Monday, August 30, 2010

Personal project: in-place list and tree management

Time is a valuable resource. Time spent cannot be regained no matter how hard you try. The only thing you can try to do is be more efficient with your time. Spend it wisely.

Since I started at Obsidian (wow, that was like 6 months ago, time flies!) I've been working non-stop. As a result, some of my personal projects have, shall we say, lagged behind the times. Interestingly enough, it's given me time to gather my thoughts on my own stuff and come up with some interesting new ideas.

One little project I've been toying around with is in-place list and tree management. This is a mundane little project that can actually have some significant impact.

Take a doubly linked list, for example. The difficult and time consuming part of doubly linked lists is getting the insert/remove operations correct. Even if you're an expert, it still takes brain power to trace the code and ensure that it's correct. After that, it's pretty mundane as it's just iteration and allocation of node space. The in-place functions (actually just inline templates) assume that you're providing the memory for the storage and do direct management of the data.

So why, you might ask, is this important or even need addressing? I mean seriously, you could either:
  1. Just write the doubly linked list when you need it and be-done-wif-it!
  2. Use a pre-canned doubly linked list and be-done-wif-it!
Say you go route number 2 and use the pre-canned doubly linked list. It allocates a node, manages next/prev pointers, and is nice and clean and happy from the front end. And then you use it in a memory allocator. Oops! You've now got the memory manager that is supposed to be managing a block of memory allocating. This causes the memory manager to call itself again, which causes a list operation, which calls the memory manager... which makes you pull your hair out as you try to resolve the circular reference.

Ok, screw that. Go with option number 1 so we can avoid the recursion. I need to write a doubly linked list using space that I've provided. Let's see, I'll create a node with next and previous pointers. Now I'll figure out the pointers to manipulate the nodes... ah that's right, three special cases, head, tail and middle... handle those... and crap now it's broke because I missed something stupid.

Ok, no, really, screw all of that. That's a recipe for disaster both from a maintenance perspective as well as from initially writing the code. K.I.S.S. How much time do you want to spend debugging something that isn't the core of your work?

Let's break it down just a touch... Option number 1 is nice in that you've got this reusable doubly linked list that allocates nodes and manages the list. Option number 2 is necessary because there are lots of situations when you don't want to allocate the node. The similarity between the two of these is that the list is managed in some known block of memory.

Easy, write this and have both Option 1 and Option 2 call it with the data to manage!

template <typename NodeType>
inline void insert_after(NodeType *target, NodeType **list)
{
NodeType *position = *list;

if (position)
{
target->next(position->next());
target->prev(position);

if (position->next())
{
position->next()->prev(target);
}

position->next(target);
}
else
{
*list = target;
target->next(0);
target->prev(0);
}
}


Not a groundbreaking idea, I know, but there are so many possibilities here... you can specialize the function for different arguments. For instance, if you know that the node that you're inserting into is not null and that the next node in the list is not null, you can do a branchless version:

template <typename NodeType>
inline void insert_after_ln(NodeType *target, NodeType *list)
{
target->next(list->next());
target->prev(list);
list->next()->prev(target);
list->next(target);
}


But wait, order now and I'll throw in this fancy little tidbit:

You can use this functionality to make a BRANCHLESS doubly linked list. Imagine, a doubly linked list that never hits a conditional. Ever! Ok, so maybe this is only really interesting to me, but on platforms like the PS3 SPU which absolutely *hate* branch prediction misses, this could be a significant gain in performance.

As to how, I'll leave that as an exercise to you... or maybe post it later today, we'll see. :)

Friday, January 22, 2010

ELF, DWARF, and *COFF* more ELF(64)

This post likely isn't going to make a lot of sense unless you really get into this niche topic... sorry all, its time for me to totally geek out.

So digging deeper and deeper into the underbelly of the beast that is the PS3 (yeah, I love it...) I'm parsing the ELF executable format in an attempt to dynamically load executables on the SPU.

"Why?!" you ask, when there are libraries that do it for us. Yes, in fact, the homebrew PS3 with Linux installed on it has the spe_context_run functions and the real devkits have SPURS. We don't "need" yet another way to load executables... do we?

Yes. And, as it turns out, no, not really. Well, the no is if you *Have* a devkit, you can just go the simple way and use SPURS. If you don't have the devkit, you could launch a pthread that loads and runs the executable on the PS3 but that is expensive as you need to reload the entire SPU each time you want to run a process. Not to mention that then you have all of these threads hanging out on the PPU just waiting and occupying stack and heap space.

So, for my purposes, yes, it is entirely necessary (from a pseudo-masochistic standpoint, but it's fun and an incredible learning experience.)

I'm not yet to the point of actually loading and executing the image yet, I'm still in the stage of processing and learning the format but I have to say it's pretty interesting. The file format is set up as a simple block loader where it has a header, program segments, section segments, and a ton of other information in it. There are extensions for different architectures and all sorts of extraneous data included in the executable that we don't need.

Oh, did I say extraneous? Why yes, I did! (aah yes, nothing like writing a blog post on a highly technical topic while half asleep at 3am before a long weekend where much packing of belongings will be involved...)

As a lot of people know, executables typically contains a TON of extra information in them that aren't needed for execution. For example, Brian Raiter wrote a very interesting (although not overly useful in daily operations) article where he reduced a simple hello world style executable from 3998 bytes to 45 bytes. Check it out at http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html.

Now, I had no desire to go to these lengths to reduce my executable size, but there is still a ton of information in them that we can get rid of.

Almost all of the ELF header can be discarded. For 64 bit images, this saves 64 bytes and for 32 bit images it saves 52. Not a lot, I know, but there are more savings to be had. Inside the executable are 'NOTE' segments that we can almost always (always?) get rid of. There are segments containing the names of segments as null terminated strings, segments that define empty segments, segments which are never referenced, etc., etc., etc.

So here's the plan... I'm going to start off simple (well... somewhat, I am loading an executable after all...) and get it to load into memory and successfully execute. After that, I'll start knocking out sections that I don't need as I figure it out. Some of it will be easy; other parts... well, not so much.

Wish me luck! (oh, and if you've done this before, please speak up as I'm having a HECK of a time trying to find documentation for some of this stuff!)

Monday, October 5, 2009

Wait-Free Cache Allocator, Part 3!

Yeah, it's been a while since I posted on this so I wanted to give a quick status on how it's working out...

There are four different types of tests:
  • Raw (r): Simple new/delete allocation scheme where each request for memory causes an allocation.
  • Blocked (b): Allocates in blocks handing out a chunk at a time. Enforces thread safety using mutexes.
  • Unblocked (ub): Just like unblocked except it is totally NOT thread safe...
  • Atomic (a): The pseudo-wait-free allocator I've been working on. (it's not actually wait free... to make it truly wait free would probably make it less efficient and take too long.)

The raw allocator is my baseline for the tests but I try to compare all of the results against each other. I'm still working on the test framework, but here are the results from a single thread running all four tests at various iterations, all of which allocate in bulk, then deallocate randomly. Yes, yes, I know, what's the point in a single threaded test of something that's multithreaded... it's just a baseline to see how it performs in non-threaded situations (where it SHOULDN'T be used, but still...)

ANYWAYS, here's the results:

000: Iters[    382]: r[ 0.0002822] b[ 0.0003256] ub[ 0.0001308] a[ 0.0001301]
001: Iters[ 573]: r[ 0.0003111] b[ 0.0004795] ub[ 0.0001159] a[ 0.0001418]
002: Iters[ 859]: r[ 0.0004065] b[ 0.0005000] ub[ 0.0001171] a[ 0.0001703]
003: Iters[ 1288]: r[ 0.0005679] b[ 0.0007953] ub[ 0.0001349] a[ 0.0004345]
004: Iters[ 1932]: r[ 0.0007991] b[ 0.0010567] ub[ 0.0001510] a[ 0.0002469]
005: Iters[ 2898]: r[ 0.0011374] b[ 0.0014791] ub[ 0.0002087] a[ 0.0003129]
006: Iters[ 4347]: r[ 0.0016636] b[ 0.0022002] ub[ 0.0001988] a[ 0.0004183]
007: Iters[ 6520]: r[ 0.0024785] b[ 0.0040991] ub[ 0.0002672] a[ 0.0005800]
008: Iters[ 9780]: r[ 0.0035872] b[ 0.0047101] ub[ 0.0003520] a[ 0.0008599]
009: Iters[ 14670]: r[ 0.0053226] b[ 0.0074197] ub[ 0.0004619] a[ 0.0011902]
010: Iters[ 22005]: r[ 0.0080567] b[ 0.0107315] ub[ 0.0008942] a[ 0.0019779]
011: Iters[ 33007]: r[ 0.0122470] b[ 0.0168040] ub[ 0.0011022] a[ 0.0027471]
012: Iters[ 49510]: r[ 0.0179474] b[ 0.0267609] ub[ 0.0018665] a[ 0.0045312]
013: Iters[ 74265]: r[ 0.0275046] b[ 0.0363858] ub[ 0.0027583] a[ 0.0066835]
014: Iters[ 111397]: r[ 0.0411089] b[ 0.0552259] ub[ 0.0036272] a[ 0.0094744]
015: Iters[ 167095]: r[ 0.0611580] b[ 0.0819954] ub[ 0.0060764] a[ 0.0145781]
016: Iters[ 250642]: r[ 0.0958014] b[ 0.1236177] ub[ 0.0086538] a[ 0.0212276]
017: Iters[ 375963]: r[ 0.1448224] b[ 0.1847220] ub[ 0.0142410] a[ 0.0322124]
018: Iters[ 563944]: r[ 0.2254061] b[ 0.3297470] ub[ 0.0198752] a[ 0.0479318]
019: Iters[ 845916]: r[ 0.3327258] b[ 0.4203867] ub[ 0.0283150] a[ 0.0709337]
TimePer: raw[0.000388] blocked[0.000516] unblocked[0.000035] atomic[0.000085]
Percent of raw: blocked[1.331635] unblocked[0.091066] atomic[0.220457]
Average: raw[0.098333] blocked[0.130944] unblocked[0.008955] atomic[0.021678]

Friday, September 18, 2009

Thread pools suck

So I was at the Guildhall mixer last night at the Austin GDC and I met Colt McAnlis, a Guildhall professor and programmer. We had a very short but very intense discussion on threading where he advised me that, in short, thread pools suck...

Which got me to thinking, why?

For those who have no idea what a thread pool is, here's the short of it... Most computers now have multiple cores, each one of which can do their own thing independently of the rest of the system. A thread pool creates a number of threads (independent tasks) usually in line with the number of cores that are available and then farms out tasks to an available pool as they become available. Of course, I'm leaving out a lot of details like hyperthreading, pseudo-threads that spend most of their time waiting, etc.

Ok, the non-geek version... You live in a house with 4 people and you've just had a party. You've accumulated a ton of dishes and you need to wash them all. "Single threaded" is one person washes, rinses, dries, and puts away all the dishes while the other three stand around and watch. "Multi-threaded" is one person washes, one rinses, one dries, and one puts them all away all at the same time. Pretty much a major improvement as the washer can keep washing while the other dishes magically make it into the cupboard.

But there's a huge gaping hole in this problem. What if the person drying takes too long and everyone else ends up waiting for him? The rinser can't rinse any more dishes because the counter is full of wet ones, the washer can't wash any more because the rinse spot is full, and the person putting them away has gone off to watch television.

What if we reassign priorities between the people. Sometimes the person washing helps to dry dishes. This works out a bit better, right? Well, what if there's only one drying towel or, even worse, the person washing is doing so because he absolutely sucks at drying them? Our improvement may give us a slight advantage, but it's not all-that-and-a-bag-of-chips... so how do we make it better?

Oh, there is so much of a better way...

Back to the geekified version because the explanation relies a lot on hardware.

A bit of background first.

Hard drives: Hard drives essentially are s.l.o.w. DVD drives are WAY worse, but trying to read data from either of these is time consuming and essentially wastes useful time while you sit there waiting for the data to come in.

System resources and shared hardware: Some system resources do not react well to being told to suddenly change tasks. The math pipeline is one that comes to mind. Lets say you queue up a bunch of math operations on a processor and it's over there grinding away on them. In the middle of the work, you tell it to stop and do this other thing instead. It has to flush every single job is has queued, cache in the new work and fill the pipeline all over again. Blech, this sucks.

So how do we avoid these scenarios?

File IO: Ideally, we'll want to serialize all of the file I/O operations per device. If we allow two threads to try to read/write to the same device at the same time (even if they're on different cores!) it will cause disk thrash. This will happen if you have multiple threads asking for data in different areas. The disk will spin to one area, read a bit, then spin to a different area, read some more, spin back to the original area and read a bit more, etc. You know this is happening when you hear a grinding sound or feel your computer vibrating when you touch it. This is ugly bad. If we restrict all of the I/O happening on a specific device to a particular thread, it will essentially serialize it all for us, optimizing the way it reads the data (assuming you're not asking for data in different areas, which I can't help you with.)

System resources and shared hardware: Using the math coprocessor as an example again, the best possible scenario here is to share the workload around to as many independent hardware pieces as possible that can do the math. For each one of the tasks, let it start, work and complete end to end so the pipeline doesn't get flushed.

But Wait, There's More!

A lot of these tasks will interfere with each other, as with the file I/O. Some of them will coexist perfectly with other types, such as mixing file I/O and math operations since they use different hardware. But here's the kicker... for some tasks, we can start it in the background and go off and do something else.

Let's mix the use of file reads and math. Traditionally, we read a file waiting for all of the data to show up then return to the caller and proceed to the next task. That is a HUGE waste of time. If we instead use asynchronous file operations, we can tell the system to queue the read then go off and do something else while we wait.

This, of course, takes some intelligence by the thread scheduler. I'm already assuming that we tag each threaded operation as the type that it's going to do (file I/O, math, etc.) When we schedule a file I/O operation, we know that it will go on a particular thread because we've chosen to serialize all of those operations. If we use asynchronous operations, we can start the task and look through our queue to find another operation that we can do while we wait for the I/O to finish.

But Wait, There's More!

Yeah yeah, I know I'm pretty much rambling at this point, but... cest la vie!

What if we give each of the threads a tiny bit of intelligence so it can schedule tasks itself grabbing them from the main pool. For example, thread 1 knows that it handles all of the file I/O for a particular device, so it pulls those tasks first. It queues the operation and immediately puts it on the back burner, requesting another task that it knows will coexist happily with the first. When that second operation finishes, it looks at the I/O task to see if it's finished with the disk operation. If it is, it finalizes the task and moves on. If it's not, it goes out and grabs another task to do.

Essentially, it puts all the scheduling logic in the threads, not the main app. The main app then just throws all of the tasks into a series of queues which can be done in linear time.

This puts traditional thread pooling on its head. Normally we have the main app schedule tasks to threads and the threads sit and wait for a semaphore to fire. Doing it this way, the main app just categorizes the tasks and the threads actively seek work. Yeah, the threads are doing more work, but if you look at Vtune or thread timings there were most likely huge gaps there anyways. The threads become more fully utilized, the main thread becomes less utilized and the tasks happen in parallel with much reduced collisions.

Friday, August 7, 2009

Long delays, but...

I'm back! Well, mostly. My new job keeps me very busy most of the time, so free time has been... well... limited.

During compiles, though, I've been working with CPUID, atomic operations (YES! STILL!!) and re-learning X86 assembly. It's been YEARS since I've written assembly, so, needless to say, I'm a bit rusty.

While I can't (and wont, so don't ask) go into what I'm working on, I've been developing for the Xbox, PS3 and Win32 simultaneously. Hopefully I can reveal what I'm doing soon, but until then... you'll just have to wait in agony.

Well, for all one or two of you that actually read my blog, that is. :)