Simulations, Memory Requirements and Dual Processors

Throughout my simulation career, it would have been easy enough to just write code, compile and simply watch it run.  But the enthusiast and speed freak within me wanted the code to go a little faster, then a little faster, until learning about types of memory and how to prioritize code became part of my standard code scenario.  There are multiple issues that all come together from all sides of the equation.

First of all, let us discuss at a high level the concept of memory caching on a pseudo-processor. 

The picture above is a loose representation of a dual core processor, with each core represented as ‘P’, Registers labeled as ‘R’, and the size of the lines is representative of the bandwidth.

The processor has access to some registers which are a high-bandwidth, low space memory store.  These registers are used to store intermediary calculation data, as well as context switching with HyperThreading.  The processor is also directly linked to an L1 (‘level 1’) cache, which is the first place the processor looks if it needs data from memory.  If the data is not in the L1 then it looks in the L2 (‘level 2’), and so on until the data is found.  Obviously the closer the data is to the processor, the quicker it can be accessed and the calculation should prove to be quicker, and thus there are large benefits to larger caches. 

In the diagram above, each processor core has its own L1 cache and L2 cache, but a shared L3 cache.  This allows each core to probe the data in L3.  What is not shown is that there are some snoop protocols designed to let each core know what is going on in another core’s L2 cache.  With data flying around it is most important to maintain cache coherency.

Take an example where we have a simulation running two threads on our imaginary processor, and each thread requires 200 kB of data.  If our L2 cache is 256 kB then the thread can easily run inside the L2 keeping data rates high.  In the event that each core needs data from the other thread, values are copied into L3 at the expense of time.

Now imagine that our processor supports HyperThreading.  This allows us to run two threads on each processor core.  We still have the same amount of hardware, but when one thread is performing a memory read or write operation, that creates a delay until the read or write operation is confirmed.  While this delay is occurring, the processor core can save the state of the first thread and move on with the second.

The downside to our new HyperThreading scenario is when we launch a program with four threads, and each thread uses 200 kB of memory.  If our L2 cache is only 256 kB, then the combined 400 kB of data spills over into our L3 cache.  This has the potential of slowing down simulations if read and write operations are very slow.  (In modern processors, a lot of the logic built into the processor is designed to move data around such that these memory operations are as quick as possible – it goes ahead and predicts which data is needed next.)

This is the simple case of a dual core processor with HyperThreading.  It gets even more complicated if you add in the concept of dual processors.

If we have two dual core processors (four cores total) with HyperThreading (eight threads), the only memory share between the processors is the main random access memory.  When a standard program launches multiple threads, there is no say in where those threads will end up – they may be run out-of-order on whatever processor core is available.  Thus if one thread needs data from another, several things may occur:

(1) The thread may be delayed until the other thread is processed
(2) The data may already be on the same processor
(3) The data may be on the other processor, which causes delays

There are many different types of simulation that can be performed, each with their own unique way of requesting memory or dealing with threads.  As mentioned in the first page of this review, even in the research group I was in, if two people wrote code to perform the same simulation, memory requirements of each could be vastly different.  This makes it even more complicated, as when moving into a multithreaded scenario the initially slower simulation might be sped up the most.

Talking About Simulations

The next few pages will talk about a different type of simulation in turn based on my own experiences and what I have coded up.  Several are based on finite-difference grid solvers (both explicit and implicit), we have a Brownian test based on six movement algorithms, an n-body simulation, and our usual compression / video editing tests.  The ones we have written for this review will be explained briefly both mathematically and in code.

Test Setup, Power Consumption, POST Time Two and Three Dimensional Explicit Finite Difference Simulations
Comments Locked

64 Comments

View All Comments

  • Hakon - Saturday, January 5, 2013 - link

    Thank you for the detailed answer. I very much appreciate your article and hope to see more stuff like this on Anandtech.

    What I meant regarding to NUMA is the following. When you have a dual socket Xeon you have two memory controllers. The first time you 'touch' a memory location it is assigned to the memory controller of the CPU that runs the current thread. This assignment is in general permanent and all further memory read/writes to that location will be served by that memory controller.

    If you first-touch (e.g. initialize the array to zero) using one thread, then the whole array is assigned to one of the two memory controllers. When you then run the multi-threaded code on that array one memory controller is idle while the other is oversubscribed since it has to serve both CPUs.

    In contrast, if you first-touch your array in an OpenMP loop and use the same access pattern as in the algorithm, you will benefit from both memory controllers later on. In this case your large array is correctly 'distributed' over both memory controllers.

    This kind of memory layout optimization becomes extremely important when you deal with quad socket Opterons. You then have eight memory controllers. A NUMA aware code is therefore up to eight times as fast since it utilizes all memory controllers.
  • toyotabedzrock - Saturday, January 5, 2013 - link

    You should go ask the people on the assembly boards for help with making your code faster.

    They are very friendly compared to a Linux kernel devs, I think they just enjoy the acknowledgement that they still exist and are useful.
  • snajpa - Saturday, January 5, 2013 - link

    Blame the scheduler. Neither Windows nor linux can effectively handle larger NUMA systems. It randomly moves the process across the physical hardware.
  • psyq321 - Sunday, January 6, 2013 - link

    Hmm, this is definitely not true at least for Windows Server 2008 R2 / Windows 7, and I am sure it holds true for some versions of Linux (I am not a Linux expert).

    Windows Server 2008 R2 / Windows 7 scheduler will try to match the memory allocations (even if they are not tagged for a specific NUMA node) with the NUMA node the process/thread resides on, and they will not move a thread to a foreign NUMA node unless if that has been explicitly requested by the application (by setting the thread affinity)

    Of course, without explicit NUMA node tagging when doing allocations, application code is the main culprit for not respecting the NUMA layout (e.g. creating bunch of threads, allocating memory from one of them - and then pinning the threads to different CPUs - you will have lots of LLC requests from remote DRAM because memory was a-priori allocated on one node).

    For this - some sane coding helps a lot, here:

    http://www.dimkovic.com/node/15

    I describe how I extracted more than double performance by careful memory allocation (NUMA-aware) - please note that neither Windows nor Linux scheduler is able to cope with code which is not written to be NUMA aware and it is using large number of threads that are supposed to run on all CPUs.. Simply put, application writer will have to manage memory allocation and usage in the way so that there are as little remote DRAM requests as possible.
  • snajpa - Sunday, January 6, 2013 - link

    About Windows scheduler - I only worked with Windows XP, now I don't have any reason to work with Win anymore, so what you say probably is really true. As for the linux versions - well, long story short, CFS sucks and everyone knows it - this is particularly noticeable if you have fully virtualized VMs which appear as one single process at the host system - the process is randomly swapped between CPU cores and even CPU dies.... sad story. That's why people have to pin their CPUs to their tasks manually.
  • psyq321 - Sunday, January 6, 2013 - link

    Ah, XP - that explains it. True, XP did not care about NUMA at all.

    Windows Server 2008 / Vista introduced NUMA-aware memory allocations, and changed their CPU scheduler so it does not move the thread across NUMA nodes. They will also try to allocate the memory from the thread's own NUMA node when legacy VirtualAlloc etc. APIs are used.

    Windows Server 2008 R2 / Windows 7 introduced the concept of CPU groups - allowing more than 64 CPUs. This does require some adaptation of the application, as old threading APIs only work with 64-bit affinity bitmask which only allowed recognizing 64 CPUs. Now, there is a new set of APIs that work with GROUP_AFFINITY structure, allowing control of CPU groups, too. However, this needs explicit change of the legacy process/threading APIs to the new ones.

    Furthermore, none of the above can replace some manual intervention*- while Windows scheduler will, indeed, respect NUMA node boundaries and not try to mess around with moving threads across them - it still does not know what the underlying algorithm wants to do.

    * There is no need to set the thread affinity to one specific CPU anymore - this prevents running the thread on any other CPU completely. Instead, there is an API called SetThreadIdealProcessor(Ex) which signals Windows scheduler that thread >should< run on that particular CPU - but, under certain circumstances the scheduler can move the thread somewhere else - if the CPU is completely taken away by some other thread/process. Scheduler will try to move the thread as close as possible - to the next core in the socket, for example - or to the next core in the group (group is always contained within a NUMA node).

    You can, however, absolutely forbid Windows scheduler from passing the thread to another NUMA node under any circumstances by simply getting the said NUMA node affinity mask (GetNumaNodeProcessorMask(Ex)) and setting this affinity as a thread affinity. This + setting the "ideal" processor still gives Windows scheduler some headroom to move the thread to another core if it is found to be better in a given moment, but it will not even attempt to cross the NUMA boundary in any case whatsoever.
  • lmcd - Monday, January 7, 2013 - link

    While I haven't personally researched them, there are tons of other schedulers that have been written for Linux and I'm certain *at least* one of them is more fitting to this line of work. I've heard of alternatives like BFS and the Linux kernel is so widely used I'm sure there's a gem out there for this application.
  • toyotabedzrock - Saturday, January 5, 2013 - link

    Have you ever tried the Intel Math Kernel Library? It might speed up some of the equations. It also hands off work to the Intel MIC card if it thinks it will speed it up.

    http://software.intel.com/en-us/intel-mkl/
  • KAlmquist - Saturday, January 5, 2013 - link

    The GA-7PESH1 motherboard is $855, and the CPU's are $2020 each, which adds up to $4895. On tasks which don't parallelize well, you can get similar performance from the i7-3770K, which costs an order of magnitude less. (Prices: i7-3770K $320, ASRock Z77 Extreme6 motherboard $152, total from motherboard and CPU $472.) On tasks which parallelize well enough that they can be run on a GPU, the system with the GA-7PESH1 will beat the i7-3770K, but will be crushed by a midrange GPU. So the price/performance of this system is pretty bad unless you throw just the right workload at it.

    The motherboard price from super-laptop-parts dot com, and the other prices are from a major online retailer that I won't name in order to get around the spam filter.
  • Death666Angel - Saturday, January 5, 2013 - link

    So, your 3770K has ECC memory or VT-D, TXT etc.?

Log in

Don't have an account? Sign up now