Program Tip

기본 new 및 delete 연산자를 대체하는 이유는 무엇입니까?

programtip 2020. 11. 21. 09:25
반응형

기본 new 및 delete 연산자를 대체하는 이유는 무엇입니까?


기본 연산자 사용자 지정 연산자를 대체 해야하는 이유 무엇 입니까?newdeletenewdelete

이것은 엄청나게 빛나는 C ++ FAQ : Operator overloading 에서 Overloading new and delete의 연속입니다
.

이 FAQ의 후속 항목은 다음과 같습니다.
ISO C ++ 표준을 준수하는 사용자 지정 newdelete연산자를 어떻게 작성해야 합니까?

참고 : 대답은 Scott Meyers의 More Effective C ++에서 얻은 교훈을 기반으로합니다.
(참고 : 이것은 Stack Overflow의 C ++ FAQ에 대한 항목 입니다.이 양식으로 FAQ를 제공하는 아이디어를 비판하고 싶다면이 모든 것을 시작한 메타에 게시 할 수 있습니다. 이 질문은 FAQ 아이디어가 처음 시작된 C ++ 채팅룸 에서 모니터링 되므로 아이디어를 제안한 사람들이 답변을 읽을 가능성이 매우 높습니다.)


다음 newdelete같은 여러 가지 이유로 연산자 를 교체하려고 할 수 있습니다 .

사용 오류를 감지하려면 :

의 잘못된 사용하는 방법이 될 것입니다 new및이 delete의 지칠대로 지친 짐승으로 이어질 수 정의되지 않은 동작메모리 누수가 . 각각의 각각의 예는 다음과 같습니다. ed 메모리 에서
둘 이상을 사용하고 사용하여 할당 된 메모리를 호출하지 않습니다 . 오버로드 된 운영자 는 할당 된 주소 목록을 유지할 수 있고 오버로드 된 운영자 는 목록에서 주소를 제거 할 수 있으므로 이러한 사용 오류를 쉽게 감지 할 수 있습니다.deletenewdeletenew
newdelete

마찬가지로 다양한 프로그래밍 실수로 인해 데이터 오버런 (할당 된 블록의 끝을 넘어 쓰기) 및 언더런 (할당 된 블록의 시작 이전에 쓰기 )이 발생할 수 있습니다 .
오버로드 된 연산자 new는 블록을 초과 할당하고 클라이언트가 메모리를 사용할 수있게 된 전후에 알려진 바이트 패턴 ( "서명")을 넣을 수 있습니다. 오버로드 된 연산자 삭제는 서명이 여전히 손상되지 않았는지 확인할 수 있습니다. 따라서 이러한 서명이 손상되지 않았는지 확인함으로써 할당 된 블록의 수명 중 언젠가 오버런 또는 언더런이 발생했는지 확인할 수 있으며 운영자 삭제는 해당 사실을 문제가되는 포인터의 값과 함께 기록 할 수 있습니다. 좋은 진단 정보를 제공합니다.


효율성 향상 (속도 및 메모리) :

newdelete운영자는하지만, 최적의 아무도를위한, 모두를 위해 합리적으로 잘 작동합니다. 이 동작은 범용 용도로만 설계 되었기 때문에 발생합니다. 그들은 프로그램 기간 동안 존재하는 몇 개의 블록의 동적 할당에서 많은 수의 단기 객체의 지속적인 할당 및 할당 해제에 이르는 할당 패턴을 수용해야합니다. 결국 컴파일러와 함께 제공 되는 운영자 new와 운영자 delete는 중간 전략을 취합니다.

프로그램의 동적 메모리 사용 패턴을 잘 이해하고 있다면 operator new 및 operator delete의 사용자 정의 버전이 기본 버전보다 성능이 더 빠르거나 최대 50 %까지 적은 메모리를 필요로한다는 것을 종종 알 수 있습니다. 물론, 당신이 무엇을하고 있는지 확실하지 않다면 이것을하는 것은 좋은 생각이 아닙니다 (관련된 복잡한 것을 이해하지 못한다면 이것을 시도조차하지 마십시오).


사용 통계를 수집하려면 :

# 2에서 언급 한 것처럼 교체 newdelete효율성 향상을 고려하기 전에 응용 프로그램 / 프로그램이 동적 할당을 사용하는 방법에 대한 정보를 수집해야합니다. 다음과 같은 정보를 수집 할 수 있습니다.
할당 블록
배포, 수명 배포,
할당 순서 (FIFO 또는 LIFO 또는 임의),
일정 기간에 따른 사용 패턴 변경 이해, 사용 된 최대 동적 메모리 양 등

또한 때로는
클래스의 동적 개체 수 계산,
동적 할당을 사용하여 생성되는 개체 수 제한 등과 같은 사용 정보를 수집해야 할 수도 있습니다 .

모든이 정보는 사용자를 대신하여 수집 될 수 newdelete과부하에 진단 수집 메커니즘과 첨가 new하고 delete.


최적의 메모리 정렬을 보상하기 new:

많은 컴퓨터 아키텍처에서는 특정 유형의 데이터를 특정 유형의 주소에있는 메모리에 배치해야합니다. 예를 들어, 아키텍처는 포인터가 4의 배수 인 주소 (즉, 4 바이트 정렬)에서 발생하거나 이중이 8의 배수 인 주소 (즉, 8 바이트 정렬)에서 발생하도록 요구할 수 있습니다. 이러한 제약 조건을 따르지 않으면 런타임에 하드웨어 예외가 발생할 수 있습니다. 다른 아키텍처는 더 관대하고 성능을 저하시키면서 작동하도록 허용 할 수 있습니다. new일부 컴파일러와 함께 제공되는 연산자 는 double의 동적 할당에 대해 8 바이트 정렬을 보장하지 않습니다. 이러한 경우 기본 연산자 대체new8 바이트 정렬을 보장하는 하나를 사용하면 프로그램 성능이 크게 향상 될 수 있으며 newdelete연산자 를 교체해야하는 좋은 이유가 될 수 있습니다 .


서로 가까운 관련 개체를 클러스터링하려면 :

특정 데이터 구조가 일반적으로 함께 사용된다는 것을 알고 있고 데이터 작업시 페이지 폴트 빈도를 최소화하려는 경우 데이터 구조에 대해 별도의 힙을 만들어서 적은 수로 함께 클러스터링되도록하는 것이 좋습니다. 가능한 한 페이지. 사용자 지정 배치 버전 newdelete이러한 클러스터링을 달성 할 수 있습니다.


색다른 행동을 얻으려면 :

때로는 연산자 new 및 delete가 컴파일러 제공 버전에서 제공하지 않는 작업을 수행하기를 원합니다.
예 : delete응용 프로그램 데이터의 보안을 높이기 위해 할당 해제 된 메모리를 0으로 덮어 쓰는 사용자 지정 연산자 작성할 수 있습니다 .


우선, 실제로 여러 가지 newdelete연산자가 있습니다 (실제로 임의의 숫자).

먼저이있다 ::operator new, ::operator new[], ::operator delete::operator delete[]. 둘째, 모든 클래스에 대해 X, 거기 X::operator new, X::operator new[], X::operator deleteX::operator delete[].

이 사이에서 전역 연산자보다 클래스 별 연산자를 오버로드하는 것이 훨씬 더 일반적입니다. 특정 클래스의 메모리 사용량이 기본값보다 상당한 개선을 제공하는 연산자를 작성할 수있는 특정 패턴을 따르는 것이 상당히 일반적입니다. 일반적으로 전 세계적으로 거의 정확하거나 구체적으로 메모리 사용량을 예측하는 것은 훨씬 더 어렵습니다.

또한 아마하지만 것을 언급 할만큼 가치의 operator newoperator new[](마찬가지로 어떤을 위해 서로 별개 X::operator newX::operator new[]), 두 가지에 대한 요구 사항 사이에는 차이가 없습니다. 하나는 단일 객체를 할당하기 위해 호출되고 다른 하나는 객체 배열을 할당하기 위해 호출되지만, 각각은 여전히 ​​필요한 양의 메모리를 수신하고 (적어도) 그렇게 큰 메모리 블록의 주소를 반환해야합니다.

요구 사항의 말하기, 그것은 다른 요구 사항을 검토 아마 가치의 1 : 글로벌 사업자가 진정한 글로벌 있어야합니다 - 네임 스페이스 내부에 1 점을 추가하는 듯하지 않을 수 있습니다 또는 특정 번역 단위에서 하나의 정적을합니다. 즉, 오버로드가 발생할 수있는 수준은 클래스 별 오버로드 또는 전역 오버로드입니다. "네임 스페이스 X의 모든 클래스"또는 "번역 단위 Y의 모든 할당"과 같은 중간 지점은 허용되지 않습니다. 클래스 별 사업자 수해야합니다 static-하지만 당신은 실제로 정적으로 선언 할 필요가 없습니다 것 - 그들은 것입니다 당신이 명시 적으로 선언 여부를 정적static또는 아닙니다. 공식적으로 전역 연산자는 모든 유형의 객체에 사용할 수 있도록 정렬 된 메모리를 많이 반환합니다. 비공식적으로 한 가지 측면에서 약간의 흔들림이 있습니다. 작은 블록 (예 : 2 바이트)에 대한 요청을 받으면 더 큰 것을 저장하려고하기 때문에 해당 크기까지 객체에 대해 정렬 된 메모리 만 제공하면됩니다. 어쨌든 정의되지 않은 동작으로 이어질 것입니다.

이러한 예비 작업을 다룬 후 이러한 연산자를 오버로드하려는 이유 에 대한 원래 질문으로 돌아가 보겠습니다 . 첫째, 전역 연산자를 오버로딩하는 이유는 클래스 별 연산자를 오버로딩하는 이유와 상당히 다른 경향이 있음을 지적해야합니다.

Since it's more common, I'll talk about the class-specific operators first. The primary reason for class-specific memory management is performance. This commonly comes in either (or both) of two forms: either improving speed, or reducing fragmentation. Speed is improved by the fact that the memory manager will only deal with blocks of a particular size, so it can return the address of any free block rather than spending any time checking whether a block is large enough, splitting a block in two if it's too large, etc. Fragmentation is reduced in (mostly) the same way -- for example, pre-allocating a block large enough for N objects gives exactly the space necessary for N objects; allocating one object's worth of memory will allocate exactly the space for one object, and not a single byte more.

There's a much greater variety of reasons for overloading the global memory management operators. Many of these are oriented toward debugging or instrumentation, such as tracking the total memory needed by an application (e.g., in preparation for porting to an embedded system), or debugging memory problems by showing mismatches between allocating and freeing memory. Another common strategy is to allocate extra memory before and after the boundaries of each requested block, and writing unique patterns into those areas. At the end of execution (and possibly other times as well), those areas are examined to see if code has written outside the allocated boundaries. Yet another is to attempt to improve ease of use by automating at least some aspects of memory allocation or deletion, such as with an automated garbage collector.

A non-default global allocator can be used to improve performance as well. A typical case would be replacing a default allocator that was just slow in general (e.g., at least some versions of MS VC++ around 4.x would call the system HeapAlloc and HeapFree functions for every allocation/deletion operation). Another possibility I've seen in practice was occurred on Intel processors when using the SSE operations. These operate on 128-bit data. While the operations will work regardless of alignment, speed is improved when the data is aligned to 128-bit boundaries. Some compilers (e.g., MS VC++ again2) haven't necessarily enforced alignment to that larger boundary, so even though code using the default allocator would work, replacing the allocating could provide a substantial speed improvement for those operations.


  1. Most of the requirements are covered in §3.7.3 and §18.4 of the C++ standard (or §3.7.4 and §18.6 in C++0x, at least as of N3291).
  2. I feel obliged to point out that I don't intend to pick on Microsoft's compiler -- I doubt it has an unusual number of such problems, but I happen to use it a lot, so I tend to be quite aware of its problems.

Many computer architectures require that data of particular types be placed in memory at particular kinds of addresses. For example, an architecture might require that pointers occur at addresses that are a multiple of four (i.e., be four-byte aligned) or that doubles must occur at addresses that are a multiple of eight (i.e., be eight-byte aligned). Failure to follow such constraints can lead to hardware exceptions at run-time. Other architectures are more forgiving, and may allow it to work though reducing the performance.

To clarify: if an architecture requires for instance that double data be eight-byte aligned, then there is nothing to optimize. Any kind of dynamic allocation of the appropriate size (e.g. malloc(size), operator new(size), operator new[](size), new char[size] where size >= sizeof(double)) is guaranteed to be properly aligned. If an implementation doesn't make this guarantee, it is not conforming. Changing operator new to do 'the right thing' in that case would be an attempt at 'fixing' the implementation, not an optimization.

On the other hand, some architectures allow different (or all) kinds of alignment for one or more data types, but provide different performance guarantees depending on alignment for those same types. An implementation may then return memory (again, assuming a request of appropriate size) that is sub-optimally aligned, and still be conforming. This is what the example is about.


Related to usage statistics: budgeting by subsystem. For instance, in a console-based game, you might want to reserve some fraction of memory for 3D model geometry, some for textures, some for sounds, some for game scripts, etc. Custom allocators can tag each allocation by subsystem and issue a warning when individual budgets are exceeded.


The operator new that ship with some compilers don't guarantee eight-byte alignment for dynamic allocations of doubles.

Citation, please. Ordinarily, the default new operator is only slightly more complex than a malloc wrapper, which, by the standard, returns memory that is suitably aligned for ANY datatype that the target architecture supports.

Not that I'm saying that there aren't good reasons to overload new and delete for one's own classes... and you've touched on several legitimate ones here, but the above is not one of them.


I used it to allocate objects in a specific shared memory arena. (This is similar to what @Russell Borogove mentioned.)

Years ago I developed software for the CAVE. It's a multi-wall VR system. It used one computer to drive each projector; 6 was the max (4 walls, floor, and ceiling) while 3 was more common (2 walls and the floor). The machines communicated over special shared memory hardware.

To support it, I derived from my normal (non-CAVE) scene classes to use a new "new" which put the scene information directly in the shared memory arena. I then passed that pointer to the slave renderers on the different machines.


It seems worth repeating the list from my answer from "Any reason to overload global new and delete?" here -- see that answer (or indeed other answers to that question) for a more detailed discussion, references, and other reasons. These reasons generally apply to local operator overloads as well as default/global ones, and to Cmalloc/calloc/realloc/free overloads or hooks as well.

We overload the global new and delete operators where I work for many reasons:

  • pooling all small allocations -- decreases overhead, decreases fragmentation, can increase performance for small-alloc-heavy apps
  • framing allocations with a known lifetime -- ignore all the frees until the very end of this period, then free all of them together (admittedly we do this more with local operator overloads than global)
  • alignment adjustment -- to cacheline boundaries, etc
  • alloc fill -- helping to expose usage of uninitialized variables
  • free fill -- helping to expose usage of previously deleted memory
  • delayed free -- increasing the effectiveness of free fill, occasionally increasing performance
  • sentinels or fenceposts -- helping to expose buffer overruns, underruns, and the occasional wild pointer
  • redirecting allocations -- to account for NUMA, special memory areas, or even to keep separate systems separate in memory (for e.g. embedded scripting languages or DSLs)
  • garbage collection or cleanup -- again useful for those embedded scripting languages
  • heap verification -- you can walk through the heap data structure every N allocs/frees to make sure everything looks ok
  • accounting, including leak tracking and usage snapshots/statistics (stacks, allocation ages, etc)

참고URL : https://stackoverflow.com/questions/7149461/why-would-one-replace-default-new-and-delete-operators

반응형