WIP: speed up Period creation
State: closed

Pending discussion on #23475, these changes demonstrate a ~35x improvement in PeriodArray creation by pushing an `isinstance(freq, DateOffset)` check from `to_offset()` to `_maybe_convert_freq()`.

```
Benchmarks that have improved:

       before           after         ratio
     [24ab22f7]       [d7cef344]
     <new_asv~1>       <_maybe_convert_freq>
           failed       2.13±0.03s      n/a  timeseries.Iteration.time_iter(<function period_range at 0x7f12102248c8>)
-      6.49±0.04s       2.21±0.03s     0.34  groupby.Datelike.time_sum('period_range')
-      1.02±0.03s         31.2±8ms     0.03  period.Algorithms.time_drop_duplicates('series')
-         172±8ms         5.21±2ms     0.03  period.PeriodIndexConstructor.time_from_pydatetime('D')
-        852±30ms         23.4±8ms     0.03  timeseries.Iteration.time_iter_preexit(<function period_range at 0x7f12102248c8>)
```

The one caveat on the benchmarks above is that they were run under Ubuntu on Windows 10, so may be substantially different than pure Linux (currently upgrading my desktop, so unavailable at the moment).

The root cause of the speedup here seems to be the elimination of 6 imports per Period object. Again, likely a Windows artifact but nevertheless seems to be an issue per cProfile:

```
%prun -s cumulative pd.core.arrays.period_array(a, freq='B')

29600097 function calls (28400097 primitive calls) in 67.949 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   67.949   67.949 {built-in method builtins.exec}
        1    0.001    0.001   67.949   67.949 <string>:1(<module>)
        1    0.000    0.000   67.948   67.948 period.py:868(period_array)
        1    0.000    0.000   67.946   67.946 period.py:199(_from_sequence)
        1    3.252    3.252   67.946   67.946 {pandas._libs.tslibs.period.extract_ordinals}    
600000/200000    4.958    0.000   58.071    0.000 <frozen importlib._bootstrap>:966(_find_and_load)  <- imports start here
600000/200000    2.527    0.000   49.730    0.000 <frozen importlib._bootstrap>:936(_find_and_load_unlocked)
400000/200000    0.738    0.000   48.856    0.000 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
400000/200000    0.949    0.000   48.490    0.000 {built-in method builtins.__import__}
   200000    3.556    0.000   28.665    0.000 <frozen importlib._bootstrap>:870(_find_spec)
   200000    0.454    0.000   19.583    0.000 <frozen importlib._bootstrap_external>:1149(find_spec)
   200000    1.482    0.000   19.129    0.000 <frozen importlib._bootstrap_external>:1117(_get_spec)
   200000    3.692    0.000   16.648    0.000 <frozen importlib._bootstrap_external>:1233(find_spec)
   600000    1.766    0.000   10.991    0.000 <frozen importlib._bootstrap>:147(__enter__)
  1000000    2.836    0.000    8.416    0.000 <frozen importlib._bootstrap_external>:57(_path_join)
   600000    3.064    0.000    6.994    0.000 <frozen importlib._bootstrap>:157(_get_module_lock)
   200000    0.430    0.000    4.989    0.000 config.py:226(__call__)
  1000000    2.739    0.000    4.630    0.000 <frozen importlib._bootstrap_external>:59(<listcomp>)
   200000    0.647    0.000    4.560    0.000 config.py:96(_get_option)
   600000    2.234    0.000    3.784    0.000 <frozen importlib._bootstrap>:176(cb)
   600000    1.157    0.000    3.362    0.000 <frozen importlib._bootstrap>:151(__exit__)
   200000    0.391    0.000    3.171    0.000 <frozen importlib._bootstrap_external>:75(_path_stat)
   200000    1.245    0.000    3.097    0.000 config.py:78(_get_single_key)
   600000    1.890    0.000    2.916    0.000 <frozen importlib._bootstrap>:58(__init__)
   200000    2.780    0.000    2.780    0.000 {built-in method posix.stat}
   600000    1.722    0.000    2.230    0.000 <frozen importlib._bootstrap>:78(acquire)
   600000    1.685    0.000    2.205    0.000 <frozen importlib._bootstrap>:103(release)
   800000    1.372    0.000    2.053    0.000 <frozen importlib._bootstrap>:847(__exit__)
   800000    1.356    0.000    2.031    0.000 <frozen importlib._bootstrap>:843(__enter__)
  2000000    1.890    0.000    1.890    0.000 {method 'rstrip' of 'str' objects}
  2000000    1.689    0.000    1.689    0.000 {built-in method _imp.acquire_lock}
  2000000    1.674    0.000    1.674    0.000 {built-in method _imp.release_lock}
  1200003    1.126    0.000    1.126    0.000 {method 'get' of 'dict' objects}
  1200000    1.027    0.000    1.027    0.000 {built-in method _thread.get_ident}
  1200000    1.026    0.000    1.026    0.000 {built-in method _thread.allocate_lock}
  1000000    0.998    0.000    0.998    0.000 <frozen importlib._bootstrap>:222(_verbose_message)
  1000000    0.993    0.000    0.993    0.000 {method 'rpartition' of 'str' objects}
  1000000    0.950    0.000    0.950    0.000 {method 'join' of 'str' objects}
   200000    0.560    0.000    0.816    0.000 config.py:550(_get_root)
   200005    0.389    0.000    0.799    0.000 <frozen importlib._bootstrap>:997(_handle_fromlist)
   200000    0.377    0.000    0.684    0.000 config.py:606(_warn_if_deprecated)
   200000    0.428    0.000    0.639    0.000 <frozen importlib._bootstrap>:416(parent)
   200000    0.428    0.000    0.633    0.000 <frozen importlib._bootstrap>:861(_find_spec_legacy)
   600000    0.628    0.000    0.628    0.000 <frozen importlib._bootstrap>:143(__init__)
   400005    0.628    0.000    0.628    0.000 {built-in method builtins.hasattr}
   200000    0.366    0.000    0.626    0.000 <frozen importlib._bootstrap>:780(find_spec)
   200000    0.352    0.000    0.583    0.000 config.py:593(_translate_key)
   400000    0.537    0.000    0.537    0.000 config.py:565(_get_deprecated_option)
   400000    0.348    0.000    0.348    0.000 {built-in method builtins.len}
   200000    0.340    0.000    0.340    0.000 <frozen importlib._bootstrap>:369(__init__)
   200000    0.313    0.000    0.313    0.000 {method 'format' of 'str' objects}
   200000    0.259    0.000    0.259    0.000 {built-in method _imp.is_frozen}
   200000    0.256    0.000    0.256    0.000 {method 'split' of 'str' objects}
   200000    0.248    0.000    0.248    0.000 <frozen importlib._bootstrap_external>:1080(_path_importer_cache)
   200000    0.237    0.000    0.237    0.000 config.py:532(_select_options)
   200000    0.205    0.000    0.205    0.000 six.py:184(find_module)
   200001    0.196    0.000    0.196    0.000 offsets.py:374(rule_code)   <- first actual work being done
   200030    0.193    0.000    0.193    0.000 {built-in method builtins.isinstance}
   200000    0.183    0.000    0.183    0.000 <frozen importlib._bootstrap>:707(find_spec)
   200000    0.176    0.000    0.176    0.000 <frozen importlib._bootstrap_external>:41(_relax_case)
        1    0.002    0.002    0.002    0.002 {pandas._libs.algos.ensure_object}
```

- [ ] waiting for discussion on #23475
- [ ] tests added / passed
- [ ] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [ ] whatsnew entry


Labels:
name: Datetime
description: Datetime data dtype
name: Performance
description: Memory or execution speed performance
name: Period
description: Period data type

Comments:
so we should benchmark a list of Periods created with an offset and from a string. not sure if these are benchmarked now.
@qwhelan can you merge master. pls retest the asv. does this show any significant speedups?
@jreback I believe the meat of the improvements were merged via https://github.com/pandas-dev/pandas/pull/23589, so I'm not seeing any significant changes when I rebase on master and re-run
Closing this as #24118 addresses the same issue more directly