14

Inspired by this question about whether the compiler can optimize away a call to a function without side effects. Suppose I have the following code:

delete[] new char[10];

It does nothing useful. But does it have a side effect? Is heap allocation immediately followed by a deallocation considered a side effect?

Community
  • 1
  • 1
sharptooth
  • 167,383
  • 100
  • 513
  • 979
  • Clarification is needed in relation to a comment I just made on dascandy's answer. When you say, "whether the compiler can...", do you mean specifically the compiler (that is, a part of the C++ implementation that operates on one TU in isolation from all other TUs), or do you actually mean to include possible link-time optimization? – Steve Jessop Jul 08 '11 at 14:14
  • 1
    @Steve Jessop: Actually I don't care - if it gets optimized it gets optimized anyway. – sharptooth Jul 08 '11 at 14:42

5 Answers5

11

It's up to the implementation. Allocating and freeing memory isn't "observable behavior" unless the implementation decides that it's observable behavior.

In practice, your implementation probably links against a C++ runtime library of some sort, and when your TU is compiled, the compiler is forced to recognize that calls into that library may have observable effects. As far as I know, that's not mandated by the standard, it's just how things normally work. If an optimizer can somehow work out that certain calls or combinations of calls in fact don't affect observable behavior then it can remove them, so I believe that a special case to spot your example code and remove it would conform.

Also, I can't remember how user-defined global new[] and delete[] works [I've been reminded]. Since the code might call definitions of those things in another user-defined TU that's later linked to this TU, the calls can't be optimized away at compile time. They could be removed at link time if turns out that the operators aren't user-defined (although then the stuff about the runtime library applies), or are user-defined but don't have side-effects (once the pair of them is inlined - this seems pretty implausible in a reasonable implementation, actually[*]).

I'm pretty sure that you aren't allowed to rely on the exception from new[] to "prove" whether or not you've run out of memory. In other words, just because new char[10] doesn't throw this time, doesn't mean it won't throw after you free the memory and try again. And just because it threw last time and you haven't freed anything since, doesn't mean it'll throw this time. So I don't see any reason on those grounds why the two calls can't be eliminated - there's no situation where the standard guarantees that new char[10] will throw, so there's no need for the implementation to find out whether it would or not. For all you know, some other process on the system freed 10 bytes just before the call to new[], and allocated it just after the call to delete[].

[*]

Or maybe not. If new doesn't check for space, perhaps relying on guard pages, but just increments a pointer, and delete normally does nothing (relying on process exit to free memory), but in the special case where the block freed is the last block allocated it decrements the pointer, your code could be equivalent to:

// new[]
global_last_allocation = global_next_allocation;
global_next_allocation += 10 + sizeof(size_t);
char *tmp = global_last_allocation;
*((size_t *)tmp) = 10; // code to handle alignment requirements is omitted
tmp += sizeof(size_t);

// delete[]
tmp -= sizeof(size_t);
if (tmp == global_last_allocation) {
    global_next_allocation -= 10 + *((size_t*)tmp);
}

Which could almost all be removed assuming nothing is volatile, just leaving global_last_allocation = global_next_allocation;. You could get rid of that too by storing the prior value of last in the block header along with the size, and restoring that prior value when the last allocation is freed. That's a pretty extreme memory allocator implementation, though, you'd need to have a single-threaded program, with a speed demon programmer who's confident the program doesn't churn through more memory than was made available to begin with.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Replacing the global `operator new` (ditto `delete` and `new[]`) is done once per program (so in any TU). However compilers are able to optimize at the link level these days. – Luc Danton Jul 08 '11 at 12:13
  • @Steve Jessop: I don't understand exactly.. Is the compiler only allowed to remove code that has no _any_ (side) effect or it also allowed to eliminate code that _may have_ side effect, but this effect clearly is not considered as esssential by programmer? (If the last is true then even default new/delete may be removed) – user396672 Jul 08 '11 at 12:59
  • 1
    @user396672: the compiler can remove code that has no effect on the "observable behavior" of the program, as defined in 1.9 of the C++03 standard. There's no such thing as "clearly not considered essential by the programmer". – Steve Jessop Jul 08 '11 at 14:07
  • 1
    @Steve Jessop: Unfortunately, "observable behavior" is too vague. How far the "observations" extends behind the C++ program itself? Does the observations include side effects of non-C++ library subroutines, possible side effects of OS calls? We usually don't consider pagefile swapping (causing by our program) as side effect. Wy consider malloc() implementation details as having side effect? – user396672 Jul 08 '11 at 15:57
  • 2
    @user396672: which part of the definition in 1.9 do you consider too vague? "Reads and writes of volatile objects and calls to I/O functions". – Steve Jessop Jul 08 '11 at 20:51
3

new[] and delete[] could ultimately result in system calls. Additionally, new[] might throw. With this in mind, I don't see how the new-delete sequence can be legitimately considered free from side effects and optimized away.

(Here, I assume no overloading of new[] and delete[] is involved.)

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • How would it be different if `new[]`/`delete[]` were overloaded? – sharptooth Jul 08 '11 at 12:05
  • @sharptooth: An extreme example would be if they consisted of empty implementations, and this was known at compile time. – NPE Jul 08 '11 at 12:09
  • @aix: As I understand, original question assumes that new and delete both _has_ side effects but their composition - (eventually) no. – user396672 Jul 08 '11 at 12:22
  • @aix: I think the program has undefined behavior if `new[]` does nothing, since it won't return different addresses each time. Obviously if the program has UB, then the optimizer can do what it likes. For my answer I tried to think of a valid implementation that would amount to a no-op in a way that I'd expect a realistic optimizer to recognize, and I haven't yet. – Steve Jessop Jul 08 '11 at 12:32
  • Code with side-effects may be optimized away. *side-effects* are different to *observable behaviour*. For example `int x = 1 + 2;` has a side-effect (updating `x`) but it has no observable behaviour if `x` is never used. – M.M Nov 04 '14 at 04:28
3

No. Neither should it be removed by compiler nor considered to be a side effect. Consider below:

struct A {
  static int counter;
  A () { counter ++; }
};

int main ()
{
  A obj[2]; // counter = 2
  delete [] new A[3]; // counter = 2 + 3 = 5
}

Now, if the compiler removes this as side effect then, the logic goes wrong. So, even if you are not doing anything, compiler will always assume that something useful is happening (in constructor). That's the reason why;

A(); // simple object allocation and deallocation

is not optimized away.

iammilind
  • 68,093
  • 33
  • 169
  • 336
  • 1
    True, but the OP's example doesn't involve user-defined types. – NPE Jul 08 '11 at 12:10
  • 1
    I don't think the compiler will assume that something useful is happening in the constructor of `char`. – Steve Jessop Jul 08 '11 at 12:13
  • @Steve, suppose if global `new[]` is overloaded and we are doing the same `counter` operation, then it does matter. – iammilind Jul 08 '11 at 12:14
  • @iammilind: it might not, for example Copy Elision can occur even when copy constructors have side effects (though this is specifically allowed by the standard). – Matthieu M. Jul 08 '11 at 12:20
  • @iammilind: sure, but at some stage in the build process, the implementation knows whether it's overloaded or not. That need not necessarily be after it's too late to optimize. The same applies to constructor side-effects, if the optimizer can determine there aren't any, then it can remove the object. – Steve Jessop Jul 08 '11 at 12:29
2

The compiler cannot see the implementation of delete[] and new[] and must assume it does.

If you had implemented delete[] and new[] above it, the compiler may inline / optimize away those functions entirely.

dascandy
  • 7,184
  • 1
  • 29
  • 50
  • I'm not entirely sure it's a correct assumption, considering new[] might throw if the system is out of memory, for example. – Vlad Ciobanu Jul 08 '11 at 11:56
  • The default (i.e. non replaced) behaviour of `operator new[]` is to delegate to `operator new` (ditto for `operator delete[]`). So for a program that does not replace `operator new[]` (the global one or the class-wide one) this is easily optimized to `operator new`. Your point still stands for `operator new` however. – Luc Danton Jul 08 '11 at 12:08
  • Regardless what they do; the compiler doesn't have the implementation handy. Even if it were an empty implementation that returns NULL - it doesn't know it so it can't remove it. – dascandy Jul 08 '11 at 13:17
  • @dascandy: sharptooth's question says "compiler", but I'm not sure whether he really means it. If so, then he doesn't care whether a link-time optimization can do it, he has a particular build chain in mind and is referring specifically to whether that build chain's compiler could do it. The standard doesn't define what a "compiler" is, though, so it's only by convention that by "compiler optimization" as opposed to "linker optimization", we mean optimizations that can be made to a single TU without seeing any other TU. – Steve Jessop Jul 08 '11 at 14:13
  • Your C library could well be - and I think libstdc++ usually is - a dynamic library, which means that it couldn't optimize it out until runtime. Unless you define your own, that is. – dascandy Jul 08 '11 at 16:48
  • Hmm, I take the question to mean "can there exist a conforming implementation that does this?", not "does my particular implementation, which allocates memory via some library or syscall, do this?" – Steve Jessop Jul 10 '11 at 18:52
2

new and delete in usual cases will result in calls to the operating systems heap manager, and this can very well have some side effects. If your program only has a single thread the code you show should not have side effects but by my observations on windows (mostly on 32 Bit platforms) show that at least large allocations and following deallocations often lead to 'heap contention' even if all of the memory is been released. See also this related post on MSDN.

More complex problems may occur if multiple threads are running. Although your code releases the memory in the meantime a different thread may have allocated (or freed) memory, and your allocation might lead to further heap fragmentation. This all is rather theoretical but it may sometimes arise.

if your call to new fails, depending on the compiler version you use probably a exception bad_alloc will be thrown and that will of course have side effects.

RED SOFT ADAIR
  • 12,032
  • 10
  • 54
  • 92