PERF: contiguity, less gil in join algos
Broken off a branch working towards #13745

This doesn't have noticeable user-facing impact on its own since this is a smaller part of the merge operation. Some timings:

```
import numpy as np
import pandas._libs.join as libjoin
np.random.seed(0)
arr1 = np.random.randint(0, 100, 100000)
arr2 = np.random.randint(0, 100, 100000)
```

Master:
```
In [2]: %timeit libjoin.inner_join(arr1, arr2, 100)
1.19 s ± 17.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit libjoin.left_outer_join(arr1, arr2, 100)
1.22 s ± 16.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit libjoin.full_outer_join(arr1, arr2, 100)
1.26 s ± 33.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

This pr:
```
In [2]: %timeit libjoin.inner_join(arr1, arr2, 100)
729 ms ± 17.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit libjoin.left_outer_join(arr1, arr2, 100)
714 ms ± 11 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit libjoin.full_outer_join(arr1, arr2, 100)
715 ms ± 6.99 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```
