After some help from the excellent Weld forum I have discovered that:
By default, singleton session beans are transactional (section 13.3.7
of the EJB 3.1 specification) and require acquisition of an exclusive
lock for every business method invocation (sections 4.8.5.4 and
4.8.5.5). In contrast, a javax.inject.Singleton is not transactional and does not support container-managed concurrency (the major
consequence being that no locking scheme is implemented by the
container).
If you annotate your singleton session bean with
@TransactionAttribute(NOT_SUPPORTED) and @Lock(READ), you should see
significantly better performance, though there may still be some
overhead. If you don't need EJB features, stick with
@ApplicationScoped (javax.inject.Singleton is not defined by CDI, and
its semantics are therefore not governed by that specification).
https://community.jboss.org/thread/213684?tstart=0
Sadly even after annotating my EJB singleton with @TransactionAttribute(NOT_SUPPORTED) and @Lock(READ) the performance was still very poor (see timings from original post).
So the take home message is don't inject EJB Singleton session beans unless you absolutely have to and even then be aware of the performance overhead that is likely to be incurred. For methods that are rarely invoked it could be negligible but as in our case small overheads accumulate rapidly if it is a heavily used method.
We did not need the EJB features and on switching to ApplicationScoped saw order of magnitude improvements in performance of the specific method that called through to the injected bean.