Building on the answers of Quuxplusone and kmdreko, here's a few more issues I noticed. I'll start with a couple of serious ones, before moving on to stylistic issues and potential optimizations:
Dead code
The only thing seriously wrong with your spin()
function is that you never call it. It's generally not very useful to ask people to review code you're not using.
(The other, minor thing wrong with it is that it seems to be written with the assumption that the input is a random integer from 1 to 10. Or maybe it's meant to be from 0 to 9, in which case the r > 5
should be r >= 5
? But either way is needlessly complicated: since you're only using the input value to make a binary choice, just have it be 0 or 1. Or you could have the input be a float between 0.0 and 1.0, if you'd like to have the ability to bias the initial magnetization state. But of course, none of that makes any difference if you don't actually use the function.)
Use of uninitialized variables
On line 29 of your main function (10 lines below //populate the lattice
), you're calling flip()
and passing beta
as an argument to it. But beta
has not been initialized at that point, since you've commented out the line that would do it!
That's a bad thing, and you shouldn't do it. Not only can the value passed to flip()
be anything (and possibly vary between runs), leading to unpredictable results, but using the value of an uninitialized variable makes the behavior of your program formally undefined, which means that the compiler is allowed to make it do anything, up to and including making demons fly out of your nose when you run it. A slightly more likely (but still protentially catastrophic) result is that the compiler might simply decide to optimize out your entire main function, since it can prove that there are no code paths through it that don't have undefined behavior!
(Of course, the out-of-bounds array access pointed out by kmdreko also leads to undefined behavior, so just setting an initial value for beta
isn't enough to fix your code.)
Confusing variable naming
In flip()
, you use H
both as an argument to the function and as the name of a local variable. While that's technically allowed, it's very confusing to read. Rename one of them.
Also, your use of temp
as the name of a temporary(?) variable in the main function is potentially confusing too, given that the surrounding code deals with (thermodynamic) temperatures. For that matter, temp
isn't a particularly informative or appropriate variable name anyway. If you really can't think of a good name for a variable, call it foo
or something so that readers can at least tell at a glance that the name means nothing. But in this case, something like iteration
would be a decently meaningful name.
Optimization opportunities
First of all, note that your code spends almost all of its time calling flip()
repeatedly, so making that function run fast should be your first optimization priority.
Getting rid of the indirect indexing via the tab
array, as suggested by kmdreko, is a good start. In the same vein, you might want to turn lattice
from a vector of vectors into a proper two-dimensional array, which will eliminated another layer of indirection.
(The down side of using two-dimensional arrays is that you'll have to know the dimensions at compile time, but in your code that's the case anyway. If you want to allow the size of the lattice to be specified at runtime, one option is to dynamically allocate a one-dimensional array of the appropriate size and treat it as a pseudo-multidimensional array by indexing it e.g. as lattice[a*N + b]
instead of lattice[a][b]
.)
You can also trivially save some memory by using signed char
instead of int
as the type of the lattice
elements. You might even want to consider using std::bitset (or implementing your own bit-packed lattice representation) to save even more memory, but this would require you to represent your lattice spin states as 0 and 1 instead of -1 and +1 (which in itself is not necessarily a bad idea at all), and likely comes at the cost of some minor speed reduction and added code complexity. For relatively small values of N
, it's probably not worth it.
Also, calculating exp(-beta*dE)
inside flip()
looks like it could potentially be somewhat slow (although, of course, you really ought to profile the code first before spending too much effort on such optimizations). It would be nice to move the exponential calculation out of the inner loop, if we can. Can we?
Expanding the definition of dE
earlier in the code (and using the definitions of the neighbor site values val1
to val4
from kmdreko's answer), the argument to exp()
is -beta * 2 * s * (H + val1 + val2 + val3 + val4)
. Here, beta
and H
do not change within the inner loop, while s
and val1
to val4
are always either +1 or -1.
The addition of the external field parameter H
to the summed spin of the neighbors makes things a bit more complicated, but we can deal with it by distributing the common -beta * 2 * s
term over the sum, giving -beta*2*s*H - beta*2*s*val1 - beta*2*s*val2 - beta*2*s*val3 - beta*2*s*val4
. Now, depending on the signs of s
and val1
...val4
, each of these terms can only take one of two values: ±beta*2*H
for the first term, and ±beta*2
for the others. If we precalculate the exponentials of each of these values outside the flip()
function (and the inner loop that calls it), we can calculate exp(-beta*dE)
simply by multiplying these precalculated terms together, e.g. like this:
void flip (float exp_2_beta, float exp_m2_beta, float exp_2_beta_H, float exp_m2_beta_H)
{
int a = (int)prandom(0, N);
int b = (int)prandom(0, N);
int s = lattice[a][b];
// calculate exp(-beta * 2 * s * (H + sum(neighbors))) based on precalculated
// values of exp(2*beta), exp(-2*beta), exp(2*beta*H) and exp(-2*beta*H):
float prob = (s != 1 ? exp_2_beta_H : exp_m2_beta_H);
prob *= (lattice[a > 0 ? a-1 : N-1][b] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a < N-1 ? a+1 : 0][b] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a][b > 0 ? b-1 : N-1] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a][b < N-1 ? b+1 : 0] != s ? exp_2_beta : exp_m2_beta);
// flip spin of this site with probability min(prob, 1.0)
if (prob >= 1 || prob >= prandom(0, 1))
{
lattice[a][b] = -s;
}
}
This makes use of short-circuit evaluation to avoid calling prandom(0, 1)
when it's not needed. Rewriting the code like this also avoids making an unnecessary write to lattice[a][b]
if the spin doesn't change. (Also, I'm assuming that N
and lattice
are global constants, since there's really no good reason for them not to be, and that you've fixed prandom()
to actually return a float.)
Now, passing all these precalculated factors as parameters to flip()
may feel a bit ugly, since they're really just an internal optimization detail. Fortunately, there's a simple fix to that: just move the inner loop (and the precalculation of those values) into the function, e.g. like this:
void do_flips (int count, float beta, float H)
{
// precalculate some useful exponential terms
float exp_2_beta = exp(2 * beta), exp_m2_beta = exp(-2 * beta);
float exp_2_beta_H = exp(2 * beta * H), exp_m2_beta_H = exp(-2 * beta * H);
// do up to (count) spin flips
for (int i = 0; i < count; i++) {
int a = (int)prandom(0, N);
int b = (int)prandom(0, N);
int s = lattice[a][b];
// calculate prob = exp(-beta * 2 * s * (H + sum(neighbors)))
float prob = (s != 1 ? exp_2_beta_H : exp_m2_beta_H);
prob *= (lattice[a > 0 ? a-1 : N-1][b] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a < N-1 ? a+1 : 0][b] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a][b > 0 ? b-1 : N-1] != s ? exp_2_beta : exp_m2_beta);
prob *= (lattice[a][b < N-1 ? b+1 : 0] != s ? exp_2_beta : exp_m2_beta);
// flip spin of this site with probability min(prob, 1.0)
if (prob >= 1 || prob >= prandom(0, 1))
{
lattice[a][b] = -s;
}
}
}
Finally, instead of "sweeping" the lattice periodically to calculate its overall magnetization state, it may be more efficient to just keep track of the sum of the spin states as you update them. For example, you could take the code above and replace the last if
statement with:
// flip spin of this site with probability min(prob, 1.0)
if (prob >= 1 || prob >= prandom(0, 1))
{
lattice[a][b] = -s;
lattice_sum += -2*s; // keep track of the sum of all the spins
}
where lattice_sum
is either a global — or, better yet, a local copy of a global variable — or passed into the function as a parameter and returned from it at the end.
Whether such real-time tracking of the total magnetization is faster or slower than periodic sweeping depends on how often you want to sample the magnetization state. With one sample per transition per lattice site, as your current code effectively does, I'd expect the real-time tracking to be somewhat faster, if only because it has better cache locality. Especially so if you make lattice_sum
a local variable in do_flips()
, which ensures that the compiler will know that it can't be aliased and can be safely stored in a CPU register during the loop.
(Also, the way you're updating the time-averaged magnetization state M
looks wonky, and I'm pretty sure you have a bug in it. To test this, try fixing M_sweep
to a non-zero constant value and see if M
correctly evaluates to M_sweep/(N*N)
. The way your code is currently written, it doesn't look like it will. Maybe you changed one of your "magic numbers" from 1000 to 500 — or vice versa — and forgot to update the other? One more reason to prefer named constants...)
spin()
function, but apparently you aren't. I'd say the first thing to start with is not asking people to review dead code. :) \$\endgroup\$