Optimize array_equivalent for NDFrame.equals
Closes #35249

```python
In [1]: import numpy as np
In [2]: import pandas as pd
In [3]: N = 10**3
In [4]: float_df = pd.DataFrame(np.random.randn(N, N))
# 1.0.5
1.52 ms ± 94 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# master
83.6 ms ± 1.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

# branch
31.2 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

And proof of this being better for short-circuiting:

```python
In [4]: N = 10**3

In [5]: a = pd.DataFrame(np.random.randn(N, N))

In [6]: b = pd.DataFrame(np.random.randn(N, N))

In [7]: %timeit a.equals(b)
# 1.0.5
1.97 ms ± 127 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [10]: %timeit a.equals(b)
56.4 µs ± 1.67 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
```

Discussion: the key thing here is that `BlockManager.equals` requires that the dtypes of `left` and `right` match. If they don't then we've already returned False. That lets us do a few things

1. Avoid many `is_foo_dtype` checks.
2. Use optimized checks like `np.isnan`
3. Avoid the slow case of considering `np.array([1, 2])` equivalent to `np.array([1.0, 2.0])`.