Include order of headers in the Math library

The confusion of the include order of headers in the Math library came up again in a pull request for Automated Style.

I’ll put some thoughts down when I get a chance. For now, here’s some older discussion we’ve had regarding this topic:

What’s the current recommendation for clients of the Math library?

We are currently encouraging clients of the Math library that need reverse-mode automatic differentiation and matrix operations to include one header file: stan/math.hpp

That should cover most of the use cases, including code generated by Stan. There are 12 different top-level include files depending on what the client needs.

The first thing that needs to be determined is whether the client needs primitives (no auto-diff, prim), reverse mode auto-diff (rev), forward mode auto-diff (fwd), or mixed mode auto-diff (for higher order, mix).

The second thing that needs to be determined is if the client needs scalars only (no std::vector or Eigen::Matrix, prim), arrays (std::vector but not Eigen::Matrix, arr), or matrix (mat). Once both things are determined, the client code just needs to include one of these headers:

  1. stan/math/prim/scal.hpp
  2. stan/math/prim/arr.hpp
  3. stan/math/prim/mat.hpp
  4. stan/math/rev/scal.hpp
  5. stan/math/rev/arr.hpp
  6. stan/math/rev/mat.hpp: this is what stan/math.hpp includes
  7. stan/math/fwd/scal.hpp
  8. stan/math/fwd/arr.hpp
  9. stan/math/fwd/mat.hpp
  10. stan/math/mix/scal.hpp
  11. stan/math/mix/arr.hpp
  12. stan/math/mix/mat.hpp

Why do we bother with 12 includes?

The reason we split prim, rev, fwd, and mix is because of compiler restrictions. In a perfect world, we’d just include mix and we’d be able to compile everywhere. Unfortunately, most Windows users would not be able to compile anything if we included mix. They should be able to include rev and fwd separately.

The main reason for splitting prim, arr, and mat is because we’ve had multiple users ask us to reduce dependencies. By that, they really mean Boost and Eigen. In prim and arr, we use Boost, but not Eigen. When we include mat, we’re now using Eigen. (I think it’d be safe for us to squish prim and arr together if we wanted.) This is something we didn’t do early on. We were able to do this a few years ago (with a lot of work). We could always reevaluate the tradeoffs in keeping this separate, but there’s a secondary reason. We may be able to cut down on the time to compilation if we keep this separate. If we don’t, then we’re always gonna be compiling the mat version.

What’s the problem?

Our template metaprograms. And our desire to want to be efficient for compile time, runtime, and developer time.

Our code base is rad. For something like our distributions, we can write one function that be used for thousands of different instantiations (and that’s not an exaggeration). For example, we have the normal lpdf, which has three arguments, y, mu, sigma. That header only includes the metaprograms from prim/scal/meta.

When it’s in prim and scal, all three of them can only be double (well… that’s not even true; they could be instantiated as float or some other type, but not from the Stan library). If we change scal to arr, then each of the three arguments could independently be std::vector<double> or double. We’d need to change the template metaprograms included to go from the scal to the arr case. These need to be instantiated before including the normal lpdf file and once that’s done, this one function can now take on std::vector arguments. And then again for arr to mat, but then we’re allowing for each of the arguments to be either double, std::vector<double>, Eigen::VectorXd, or Eigen::RowVectorXd.

But wait… there’s more! Change prim to rev and now we can replace double with stan::math::var. That requires a different set of the template metaprogram instantiations. And so on for fwd and mix.

So it’s really our template metaprograms. We can’t just include the right metaprograms for mix and mat, although that’s the easiest for the devs, because if we did that, we’d alienate a lot of users who wouldn’t be able to even compile the math library.

And include what you use is actually pretty tough. We’d have to expect the devs to know which includes are necessary for the 12 different folders. It’s not impossible, but it does require thought. We could have 12 different meta includes and I’m pretty sure that would fix it. It’d be pretty easy to replace the overall header with the meta header.

2 Likes

I think for clients, it is not a big deal to include stan/math.hpp. For the unit tests, however, Stan developers should be able to include the necessary and sufficient headers in the right order with appropriate guidance. So, foo_test.cpp should include foo.hpp, gtest/gtest.h, and not much else.

1 Like

Spot on. The problem is that we’ve tried putting in guidance for what’s necessary, but it’s hard now. And it’s changed over the years. It’s been discussed on the lists before and it’s documented in different places, but I think there are only a few of the devs that have worked through the metaprograms well enough to know how it’s pieced together. I still have to look at examples to get it right.

This was one of the decisions we made on the dev list and it went this way to help existing and new devs. We had valued ease of writing a new test over compiler time. I’m still writing more thoughts and I’ll update the first post.

But… just wanted to point out some of the difficulty with your example. We are now writing pretty damn complicated templated code. The way we write things now, foo.hpp would typically be inside stan/math/prim/scal.hpp with a function signature like:

template <T>
T foo(T& bar);

To test this, we really want to write 12 tests (these line up with the 12 headers above and live in 12 different folders). Each of those 12 files needs to include a different set of template metaprograms included, in addition to foo.hpp and gtest/gtest.h, in order for this to work. In some of the cases, the compiler won’t gripe, but the runtime behavior will be wrong. In other cases, the compiler will throw up error messages.

To get down to include what you use, we’d need to only include the template metaprograms in use and then it gets really tricky. The compromise is to have 12 headers for all the template metaprograms and have each of the tests include those and foo.hpp. That should work out ok.

It seems as if the Makefiles should be able to include the correct -include /path/to/metaprograms.hpp statement that is appropriate for the directory under test/unit where foo_test.cpp is. At least, there would be less burden on the developer if that were so.

Whoa. That does sound like something that’s feasible. We’d first need to create the right includes. We don’t have them now. Secondly, I don’t know enough about the include mechanism to know whether or not that would work. Is it guaranteed to include that -include argument before the test file that’s being compiled? If so, then we can make it work. It would take a bit of effort to get the makefile correct. We’d have new dependencies, but I’ll assume we can figure it out.

Btw, glad you’re contributing to math! I wish you brought this up years ago!

-include file
Process file as if “#include “file”” appeared as the first line of the primary source file. However, the first directory searched for file is the preprocessor’s working directory instead of the directory containing the main source file. If not found there, it is searched for in the remainder of the “#include “…”” search chain as normal. If multiple -include options are given, the files are included in the order they appear on the command line.

1 Like

Thanks for summarizing all this, Daniel! This is super helpful. Should it instead go on the Wiki?

I have mixed feelings about this. We used to do this, but now recommend the tests also include the monolithic math.hpp (or whatever they need) because we were missing cases where include order conflicts in math.hpp caused things not to work when it worked with developer-defined include orders in the tests.

You’re welcome.

Yes, I think that’s a good place. Want me to put it up?

Me too.

I think if we have the traits included in the right order, we might be able to include what you use in the tests. I think it’s only the traits that we need to be careful about instantiating in the correct order.

Yes, please.

I just put up a new page in the math wiki here: Include order of headers

It’s the first post in this thread with a minor edit to the beginning just copied in to the wiki.

When I went to the math wiki, a lot of this information is sitting there at the home page. The relevant part to this discussion is in this section: Order Dependencies in Eigen Traits.

4 Likes

Thanks!

1 Like