Imperative Sieve of Eratosthenes using destructive updates in Haskell
takeWhile (&lt;=n) $ iterate (+k) (k+k) can be written as [2*k, 3*k..n] which, here, can be replaced with [k*k, k*k+k..n]. 

Similarly, takeWhile (&lt;= kMax) . iterate (+1) $ 2 can be written as [2..kMax]. 

Algorithmically, we can work with odds only, and switch to [k*k, k*k+2*k..n] and [3,5..kMax] (special-casing the 2). Taking it further to pre-ignoring the multiples of 3 (5,7...) also, this is known as wheel factorization optimization.

mapM and mapM_, though very idiomatic, can be faster if coded manually as loops (recursive functions). This would also enumerate the numbers manually, instead of creating the interim lists and hoping the compiler won't actually  create them.

Using unsafeRead and unsafeWrite should be faster than the safe variants. 

Lastly, last . primesSmallerThanOrEqualTo can be replaced with lastPrime,

lastPrime n = let arr = sieve n in
              take 1 [ i | i &lt;- [n, n-1..2], arr ! i]


Even faster is to move the last prime-finding code into the sieve function itself, and return that prime from it, instead of the array.