0

So I've got a rendering engine where in certain applications, it makes sense to use a vertex buffer, while in other applications it makes sense to use a triangle buffer. I'll briefly explain the difference so it is clear:

  • Vertex Buffer: This approach means that triangles are stored using 2 arrays. A vertex buffer and an index buffer. The vertex buffer contains all of the unique vertex information, while the index buffer contains all of the face definitions as indices of the vertex buffer. So for example, to obtain the 2nd vertex in a triangle with index idx you would call: vertex_buffer[index_buffer[3*idx+1]] (assuming both vertex_buffer and index_buffer are just 1d arrays).

  • Triangle Buffer: This approach means that each triangle is represented by a struct containing all of its necessary data. So to access the first element of the 2nd vertex in a triangle with index idx you would call: triangle_buffer[idx].vertex1[0].

The benefit of the triangle buffer approach is that it requires fewer reads from memory for each individual triangle at the expense of potentially duplicating data (as is the case if you have a continuous geometry and so triangles share vertices). The benefit of the vertex buffer approach is that it does not duplicate any memory at the expense of needing more reads from memory to get both the index and subsequent vertex. For a continuous geometry processed coherently though, this is less of an issue as previously loaded triangle vertices will still be in cache.

There are times when it makes more sense to use one of these approaches over the other, and I'd like that to be configurable at compile time. My first gut thought on how to do that would be to create a wrapper function for the this process of fetching triangle data, and inlining that where needed. So the function(s) might look like this:

template <typename Scalar>
inline Vertex<Scalar> get_triangle_vertex0(size_t triangle_idx) {
    #if defined(TRIANGLE_BUFFER)
        return triangle_buffer[idx].vertex0
    #else
        return vertex_buffer[index_buffer[3*idx+1]]
    #endif
}

template <typename Scalar>
inline Vertex<Scalar> get_triangle_vertex1(size_t triangle_idx) {
    #if defined(TRIANGLE_BUFFER)
        return triangle_buffer[idx].vertex1
    #else
        return vertex_buffer[index_buffer[3*idx+1] + 1]
    #endif
}

NOTE: For this is just rough pseudo code just meant to outline the rough concept of my initial plan, which is to wrap each of the indexing methods in an inlined function so that at compile time you can decide between each method.

Is this the correct way of handling this sort of thing? Is there a performance penalty for this? Or is there a more sensible way to achieve the same result?

The only alternative I can think of is to go through and manually put the preprocessor directives everywhere I need to access triangle data. Which is why I thought an inline function like this might help consolidate that and keep things clean.

Chris Gnam
  • 311
  • 2
  • 7
  • @SamVarshavchik I apologize, I fixed the typo. – Chris Gnam Feb 06 '23 at 18:10
  • 2
    `inline` is a **hint** to the compiler that a function should be expanded inline; as such, it has no enforceable consequences. Its visible meaning is that the same definition can occur in multiple translation units, and the compiler is free to pick any one of them. Template functions, like your `get_triangle_vertex`, are always inline in that sense. Whether the compiler expands the function inline is up to the compiler. In short: you don't need to mark this function `inline`. Chances are the compiler will see that it's just a wrapper and expand it inline. – Pete Becker Feb 06 '23 at 18:10
  • 1
    And, yes, the approach you're taking makes sense. This kind of code is quite common. – Pete Becker Feb 06 '23 at 18:12
  • Is this true even when one explicitly instantiates certain types? I frequently declare templated functions/classes in the headers, then in the implementation file I explicitly declare the types. (I do this whenever possible as I like to keep my implementation separate and to ensure the templated class is only used with the intended types) – Chris Gnam Feb 06 '23 at 18:14
  • @ChrisGnam -- re: "is this true..." -- is **what** true? What you describe in your most recent comment sounds more like function overloading, complicated by unnecessary (ab)use of templates. – Pete Becker Feb 06 '23 at 18:25
  • @ChrisGnam Explicit instantiation is pointless for `inline` function templates (It has no effect, basically.). If you have the definitions required for the instantiation in only one translation unit, then the compiler can't inline without link-time optimization into other translation units either, anyway, regardless of `inline`. – user17732522 Feb 06 '23 at 18:26
  • I'm not sure how it sounds like function overloading. I'm simply keeping the implementation of templated types separate from the declarations: https://stackoverflow.com/questions/2351148/explicit-template-instantiation-when-is-it-used – Chris Gnam Feb 06 '23 at 18:28
  • 1
    @ChrisGnam That's reducing compile-time at the cost of optimization opportunities (specifically inlining). If you use that approach the `inline` keyword doesn't really make sense at all in the context (and I am not even sure whether it is allowed to declare, but not define, an `inline` template in a translation unit where it would be implicitly instantiated). – user17732522 Feb 06 '23 at 18:30
  • 1
    `inline` is not a hint to the compiler, it's a linker directive that indicates that a function is defined in multiple translation units (and is really just used to get around the one-definition-rule). For template functions it is pointless, they already behave as if they're inline. – Kyle Feb 06 '23 at 18:43
  • 1
    Are all the translation units involved either compiled with `-DTRIANGLE_BUFFER` or `-UTRIANGLE_BUFFER`? Without mixing some of one and some of the other? – Eljay Feb 06 '23 at 18:50
  • @Kyle -- re: "`inline` is not a hint to the compiler" -- the standard disagrees with you. "The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism. An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions defined by 7.1.2 shall still be respected." [dcl.fct.spec]/2. – Pete Becker Feb 06 '23 at 20:06
  • @ChrisGnam -- with no definition visible at the point of call and explicit instantiation in a single implementation file you have a concrete set of functions that can be used in the program, not an open-ended set defined by a template. That's why it sounds like function overloading. But more to the point: are you solving an important problem? I've never had a situation where hand-tweaking template instantiations provided a build-time improvement that was significant enough to offset the build- and maintenance-headaches that hand instantiation creates. – Pete Becker Feb 06 '23 at 20:25
  • @PeteBecker Fair enough, but the important part of that clause is that "other rules for inline functions" part. The hinting bit doesn't enforce anything since implementations are free to ignore it. Historically that was the purpose of the specifier. The meaning of inline has since come to be more aligned with the meaning I gave. This is seen with the introduction of inline variables which, as far as linking is concerned, have basically the same semantics as inline functions. This meaning is also consistent with attempting to define a function in a header, the inline specifier is *required*. – Kyle Feb 07 '23 at 17:59
  • @Kyle -- if you had actually **read** my initial comment you'd have seen that what I said is exactly correct (although a bit inartfully worded). Your subsequent explanation adds nothing to what I originally said. And since the question is explicitly about "inlining" a wrapper function, it's important to talk about inline expansion. Thanks for the history lesson, but I'm well aware of the meaning of "inline" and how it's developed. I was project editor for the C++11 standard, and I know my way around the standard far better than you do. – Pete Becker Feb 07 '23 at 19:12

0 Answers0