Friday, March 26, 2010

Things that don't work on Xbox

Although it is possible to write F# code that runs on the Xbox 360 using XNA Game Studio, not everything works.

There are API problems, probably due to the fact that the F# core assembly I am using is built for the compact framework. Although the .NET framework on the Xbox 360 is based on the compact framework, there are obviously significant differences.
I hope I will be able to take a look at this when Microsoft releases the source code of the F# code library in an easily compilable form (the F# team has said that they intended to release the F# compiler and libraries under an Open Source license when it reaches a stable state).

For instance a call to sprintf (for debugging purposes) resulted in an exception, due to a missing method, it seems.

Similarly, attempts to use async workflows also fail. This is really a shame, as it means you are on your own when it comes to concurrent applications. For instance, the map_parallel function I used in tutorial 7 raises an exception.

In addition to binary compatibility issues specific to F#, there are also generic practical issues.

After implementing my own version of Async.Parallel, I realized it probably would not be much help, as the overhead of multi-threading is a bit too much for the time intervals of computations which must run in a 60th of a second (16.7 ms). Using parallel_map resulted in a factor two performance drop, in my case. For those interested, here is the code I was using (no guarantee of correctness of any kind!):


let map_parallel (func : 'T0 -> 'T1) (data : 'T0[]) : 'T1[] =
let results = Array.zeroCreate data.Length
let num_threads = 4
let ranges = Array.init num_threads (fun i -> i * data.Length / num_threads, (i + 1) * data.Length / num_threads - 1)

let count = ref num_threads
let notify = new System.Threading.AutoResetEvent(false)

for (start, stop) in ranges do
System.Threading.ThreadPool.QueueUserWorkItem (
fun _ ->
try
for i in start..stop do
results.[i] <- func data.[i]
finally
let n = System.Threading.Interlocked.Decrement count
if n = 0 then
System.Threading.Thread.MemoryBarrier()
notify.Set() |> ignore
)
|> ignore

notify.WaitOne() |> ignore
results


What works well is to dispatch rendering and updating on two threads. The rendering thread must remain on the "main" thread, the updating thread can run on a separate thread.

Is F# worth the trouble on Xbox? I would say it is. Compared to C#, the language has good support for manipulating arrays, an "inline" keyword that comes very handy when writing generic code that must run fast. This makes it a good language for computation-heavy applications (of which simulation-oriented games are).

Another area where the language should shine is in high-level UI code. I have thoughts about using custom workflows to conveniently map operations spanning over multiple frames on the game update loop.

3 comments:

elder_george said...

ThreadPool.QueueUserWorkItem guarantees that delegate will be executed in a thread different from main. It doesn't guarantee that each passed delegate will be executed in parallel.

Have a look at this program http://pastebin.com/gHdWZh1s, for example.

AFAIK, async delegates don't have this problem.

Joh. said...

I did not understand what the code on pastebin was supposed to demonstrate.

The first experiment measures the computation time on the full array.

The second experiment measures the computation time on the first half of the array, using one thread from the thread pool.

The last experiment measures the time on both halves, using two threads, each working on its own half.

Is the second experiment as intended?

elder_george said...

@Joh

Silly me, I've made such a stupid mistake...

Yes, it seems that I was wrong and there's no difference between running task in thread pool or running part of it in main thread. Sorry.

Here's corrected version for using ThreadPool:

http://pastebin.com/c0Q9zmqp

and one using async delegates

http://pastebin.com/bMHNJakz

They show that async delegates (strangely enough) perform slightly better, but it is largely insignificant.

It would be interesting to know how this approaches will work on XBox's CLR and will their performance differ.