3

While reading this question, I've seen the first comment saying that:

size_t for length is not a great idea, the proper types are signed ones for optimization/UB reasons.

followed by another comment supporting the reasoning. Is it true?

The question is important, because if I were to write e.g. a matrix library, the image dimensions could be size_t, just to avoid checking if they are negative. But then all loops would naturally use size_t. Could this impact on optimization?

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
Costantino Grana
  • 3,132
  • 1
  • 15
  • 35
  • 2
    Many people, including branje stroustrup feel that using a unsigned size was a mistake. I happen to agree with that and I like to use `ptrdiff_t` as a container size. – NathanOliver Jan 17 '19 at 20:15
  • 1
    You could just name an alias for your size type (ex. `using my_size_type = std::size_t;`). This allows you to easily change which type you use. You could then measure the performance of both. Just watch out that you don't make assumptions about `my_size_type` in your code. – François Andrieux Jan 17 '19 at 20:15
  • Length of what? If you are using standard container, you should use their `size_type`. If you are measuring size of the object, you should use type which `sizeof` yields. And so forth. – SergeyA Jan 17 '19 at 20:20
  • 1
    When the relevant optimizations fail to kick in the difference (in my experience) is generally negligible. I agree with @SergeyA in that I usually use whatever type matches what my `class` will likely interact with the most. Though others may have had different experiences. – François Andrieux Jan 17 '19 at 20:22
  • I am considering VTC as 'primary opinion-based'... Any reason I should not? – SergeyA Jan 17 '19 at 20:23
  • @FrançoisAndrieux in a strictest of senses, yes. But it seems to become opinion based once you go past the simple yes, do you think so? I am not sure though, this is why I dind't vote yet. – SergeyA Jan 17 '19 at 20:24
  • @NathanOliver I coudn't find a quote of Stroustrup saying that. Do you have any pointer/link/detail? – Costantino Grana Jan 17 '19 at 20:30
  • @SergeyA The question was originally tagged C also. If I'm writing a library, is there any reason for not using size_t for lengths? From your comments and the answer it seems to me that the answer is: "no". – Costantino Grana Jan 17 '19 at 20:33
  • 2
    You can get a lot of background and related information from [this question](https://stackoverflow.com/questions/10168079/why-is-size-t-unsigned). – François Andrieux Jan 17 '19 at 20:35
  • @FrançoisAndrieux Thank you! – Costantino Grana Jan 17 '19 at 20:40
  • @NathanOliver do you have any good links about the subject? About unsigned size being a mistake, that is. – Ramon Jan 17 '19 at 20:43
  • Have a look at https://stackoverflow.com/questions/49782609/performance-difference-of-signed-and-unsigned-integers-of-non-native-length I stand by my comment. – Matthieu Brucher Jan 17 '19 at 20:44
  • 5
    @Ramon and the OP: https://www.youtube.com/watch?v=Puio5dly9N8#t=42m40s – NathanOliver Jan 17 '19 at 20:47
  • Wow. Look at those young punks. – user4581301 Jan 17 '19 at 22:55

3 Answers3

6

size_t being unsigned is mostly an historical accident - if your world is 16 bit, going from 32767 to 65535 maximum object size is a big win; in current-day mainstream computing (where 64 and 32 bit are the norm) the fact that size_t is unsigned is mostly a nuisance.

Although unsigned types have less undefined behavior (as wraparound is guaranteed), the fact that they have mostly "bitfield" semantics is often cause of bugs and other bad surprises; in particular:

  • difference between unsigned values is unsigned as well, with the usual wraparound semantics, so if you may expect a negative value you have to cast beforehand;

    unsigned a = 10, b = 20;
    // prints UINT_MAX-10, i.e. 4294967286 if unsigned is 32 bit
    std::cout << a-b << "\n"; 
    
  • more in general, in signed/unsigned comparisons and mathematical operations unsigned wins (so the signed value is casted to unsigned implicitly) which, again, leads to surprises;

    unsigned a = 10;
    int b = -2;
    if(a < b) std::cout<<"a < b\n"; // prints "a < b"
    
  • in common situations (e.g. iterating backwards) the unsigned semantics are often problematic, as you'd like the index to go negative for the boundary condition

    // This works fine if T is signed, loops forever if T is unsigned
    for(T idx = c.size() - 1; idx >= 0; idx--) {
        // ...
    }
    

Also, the fact that an unsigned value cannot assume a negative value is mostly a strawman; you may avoid checking for negative values, but due to implicit signed-unsigned conversions it won't stop any error - you are just shifting the blame. If the user passes a negative value to your library function taking a size_t, it will just become a very big number, which will be just as wrong if not worse.

int sum_arr(int *arr, unsigned len) {
    int ret = 0;
    for(unsigned i = 0; i < len; ++i) {
        ret += arr[i];
    }
    return ret;
}

// compiles successfully and overflows the array; it len was signed,
// it would just return 0
sum_arr(some_array, -10);

For the optimization part: the advantages of signed types in this regard are overrated; yes, the compiler can assume that overflow will never happen, so it can be extra smart in some situations, but generally this won't be game-changing (as in general wraparound semantics comes "for free" on current day architectures); most importantly, as usual if your profiler finds that a particular zone is a bottleneck you can modify just it to make it go faster (including switching types locally to make the compiler generate better code, if you find it advantageous).

Long story short: I'd go for signed, not for performance reasons, but because the semantics is generally way less surprising/hostile in most common scenarios.

Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • 2
    It can. The compiler will unroll loops with signed integers, and not with unsigned ones, generate SIMD. That's a BIG difference. That's not overrated. – Matthieu Brucher Jan 17 '19 at 21:10
  • With `-Wsign-conversion` (highly recommended), codes such as `sum_arr(some_array, -10);` will generate a warning. I think that implicit signed/unsigned conversions are bad, so I always enable this warning. Whenever I want a signed/unsigned conversion, I use an explicit cast. – geza Jan 17 '19 at 21:16
  • 2
    @MatthieuBrucher I wouldn't say so https://gcc.godbolt.org/z/jw4qIT unrolled and SIMDed just the same. If your compiler is bad it's not `unsigned`'s fault. – Matteo Italia Jan 17 '19 at 21:20
  • 1
    @MatthieuBrucher your comment seems to be based on a single compiler (Clang) which is known for it's hostility toward unsigned numbers. Your own example below shows the same codegen with gcc and icc. – SergeyA Jan 17 '19 at 21:21
1

That comment is simply wrong. When working with native pointer-sized operands on any reasonable architectute, there is no difference at the machine level between signed and unsigned offsets, and thus no room for them to have different performance properties.

As you've noted, use of size_t has some nice properties like not having to account for the possibility that a value might be negative (although accounting for it might be as simple as forbidding that in your interface contract). It also ensures that you can handle any size that a caller is requesting using the standard type for sizes/counts, without truncation or bounds checks. On the other hand, it precludes using the same type for index-offsets when the offset might need to be negative, and in some ways makes it difficult to perform certain types of comparisons (you have to write them arranged algebraically so that neither side is negative), but the same issue comes up when using signed types, in that you have to do algebraic rearrangements to ensure that no subexpression can overflow.

Ultimately you should initially always use the type that makes sense semantically to you, rather than trying to choose a type for performance properties. Only if there's a serious measured performance problem that looks like it might be improved by tradeoffs involving choice of types should you consider changing them.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • `unsigned` integers have extra guaranties and fewer causes for undefined behavior. That can get in the way of the compiler. With `int` the compiler can assume it never overflows and act as-if it goes on forever since if it *does* overflow you have UB and whatever happens is allowed. With `unsigned` integers the compiler has to ensure that the wrap-around behavior is preserved. – François Andrieux Jan 17 '19 at 20:28
  • 1
    This answer is completely wrong. Have you checked what the generated code looked like? There is no difference at the machine level, there is a difference at the C++ level and thus on how the compiler will behave. – Matthieu Brucher Jan 17 '19 at 21:09
  • @FrançoisAndrieux: That is true but not going to have any practical relevance in the OP's case. – R.. GitHub STOP HELPING ICE Jan 17 '19 at 23:16
  • See also Matteo's answer which agrees that performance is not a significant issue here. – R.. GitHub STOP HELPING ICE Jan 17 '19 at 23:18
1

I stand by my comment.

There is a simple way to check this: checking what the compiler generates.

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

So what does the compiler generate? I would expect loop unrolling, SIMD... for something that simple:

Let's check godbolt.

Well, the signed version has unrolling, SIMD, not the unsigned one.

I'm not going to show any benchmark, because in this example, the bottleneck is going to be on memory access, not on CPU computation. But you get the idea.

Second example, just keep the first assignment:

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

As you want gcc

OK, not as impressive as for clang, but it still generates different code.

Matthieu Brucher
  • 21,634
  • 7
  • 38
  • 62
  • 2
    Both gcc and icc generate identical (at least when eyeballing) code. The fact that CLang chooses different codegen tells us about CLang, not signed vs unsigned. – SergeyA Jan 17 '19 at 21:15
  • ICC is known to be very aggressive, and sometimes too much, even generating bad code. I'll do some more digging for gcc. – Matthieu Brucher Jan 17 '19 at 21:25
  • There is nothing aggressive about ICC in provided example, in my view. I heard some talks by LLVM guys, in my view they took the mantra "never used unsigned integers" to close to heart and just decided to make them second class citizens. And as for *bad code* - we build our production builds with ICC for a long time. I am yet to see an example of bad codegen (as in - working incorrectly). – SergeyA Jan 17 '19 at 21:26
  • Added another example on GCC, simpler, still different generated code. Also ICC generates bad code for `std::sort` on trivial pairs of ints... – Matthieu Brucher Jan 17 '19 at 21:28
  • 1
    Code is different, unsigned version is shorter. As for actual performance I have no idea. What do you mean by bad code? A code which produces results which Standard doesn't prescribe? Can you share the example? – SergeyA Jan 17 '19 at 21:31
  • 1
    If anything, in your second example the `unsigned` version looks slightly better (and is slightly shorter, which never hurts for i-cache usage), although I'd bet that they will perform pretty much the same. But again, the point is that generally the difference in performance is below the noise floor, so it shouldn't be a deciding factor - correctness and ease of use are way more important. You can always spend an afternoon optimizing a specific loop if it is actually slower than you'd like. – Matteo Italia Jan 17 '19 at 21:32
  • Unfortunately, no, I cannot show the code that makes icc generate wrong code here. – Matthieu Brucher Jan 17 '19 at 21:35
  • But I've seen bad code generated by the C++ Intel compiler very often, it's far less stable than the Fortran one. Unfortunately. – Matthieu Brucher Jan 17 '19 at 21:46