Introduce SIMD intrinsics for `_dist_metrics.pyx`

# Context
Pairwise distance computation is an essential part of many estimators in scikit-learn, and can take up a significant portion of run time in certain workflows. I believe that we may achieve significant performance gains in several (perhaps most) distance metric implementations by leveraging SIMD intrinsics.

# Proof of Concept

I built a quick proof of concept just to see what kinds of performance gains we could observe with a potentially-naive implementation of SIMD intrinsics. I chose to optimize the `ManhattanDistance.dist` function. This implementation uses intrinsics found in `SSE{1,2,3}`. To ensure that the instructions are supported, it checks for the presence of the `SSE3`  instruction set (`SSE3` implies `SSE{1,2}`) and provides the optimized implementation if so. Otherwise it provides a dummy implementation just to appease Cython, and the main function falls back to the current implementation on `main`. Note that on most modern hardware, support for `SSE3` is a reasonable expectation (indeed numpy assumes it is always present when optimization is enabled). For the specific implementation referred to here, please take a look at this PR: https://github.com/Micky774/scikit-learn/pull/11

Note that the full benefit of the intrinsics are gained when compiling with `-march="native"`, however the benefit is still significant when compiling with `-march="nocona"`, as is often default (e.g when following the scikit-learn development instructions on linux).

# Benchmarks
The following benchmarks were produced by this gist: https://gist.github.com/Micky774/567a5fa199c05d90c4c08625b077840e

### **Summary: The SIMD implementations are ~2x faster than the current implementation for `float32` and 1.5x faster for `float64`.**

<details>
<summary>Plots</summary>

![f2b1f1e8-59b0-4ec5-b91c-fe1d19abd9ec](https://user-images.githubusercontent.com/34613774/228374474-99ee13b3-228d-4c53-9dcc-3a8f8b639a9f.png)
</details>

# Discussion
I haven't looked too deeply into this yet, as first I wanted to see whether there was interest in the venture. I would love to hear what the other maintainers' thoughts are regarding exploring this route in a bit more detail. Obviously SIMD implementations will bring with them added complexity, but the performance gains are pretty compelling. In my opinion, the tradeoff is worth it.

CC: @scikit-learn/core-devs

COMMENT:
> But these are compile-time checks right? So if the binaries are compiled on a machine that supports SSE3, the binary will try to execute the intrinsics instructions, even if the end-users' machine doesn't support it?

Yes. Regarding this, we could either follow NumPy's strategy of _build the superset and dispatch at runtime_, or _build minimally_ and specify which features we enable to try to maximize portability. The PIL-SIMD discussion is based on enabling `AVX2` however I don't think we would _need_ to if we wanted just a minimal optimization.

With that being said, NumPy and PyTorch have already built out viable runtime dispatch systems for SIMD intrinsics. While we can't quite apply their solutions one-to-one (and even if we could it would still be a lot of effort), we have a lot we could learn from them if we did decide to pursue that solution. As an aside, I'm personally more familiar with NumPy's system.

Just to minimize scope right now, I'd rather stay within `SSE3` instructions since those are still incredibly portable afaik.

> What I am wondering is if these 2x and 1.5x are also obtained at the level of the estimator. My feeling here is that we will introduce a layer of complexity that we probably want to extend to a lot of subroutines. Is the complexity worth the 1.5x to 2x speed-up?

Almost surely the gains would not be that dramatic for _all_ estimators  (or perhaps even _most_ estimators) however this would help those where distance computations are a potent bottleneck. I imagine such estimators would be mostly those which now use the `PairwiseDistanceReduction` backend. Consequently, I think distance computation will steadily become more present as bottlenecks over time.

> I think the main resistance would be to adding new c/c++ code to the
codebase due to the maintenance burden on new contributors. (not sure how
our stance on that changed since ive last been active though!)

Thanks for offering this point. It is one of my greatest apprehensions towards this as well. I feel like only recently with the immense work of several maintainers has the momentum for Cython contributions been increasing (shoutout to @thomasjpfan and @jjerphan for helping me learn Cython) and yet it still remains a rare target for non-maintainer contributions. The introduced SIMD intrinscs and surrounding C code would be even harder to foster support for.

With that being said, I think thorough education -- both in documentation and reviews -- can help mitigate that. I think if we keep the use in a very limited scope and focus on clarity, then we can even develop those skills in our community. I've done some work on the NumPy side of things, and I do adore that about their community: a lot of folks are eager to learn new skills when fumbling around the codebase and seeing syntax they had never seen before :)

Labels: Enhancement, Needs Decision, cython