C ++에서 벡터를 통해 목록을 사용하는 요점은 무엇입니까?


32

C ++ 목록 및 벡터와 관련된 3 가지 실험을 실행했습니다.

벡터가있는 사람들은 중간에 많은 삽입이 관여 된 경우에도 더 효율적인 것으로 판명되었습니다.

따라서 질문 : 어떤 경우에 목록이 벡터보다 더 의미가 있습니까?

대부분의 경우 벡터가 더 효율적으로 보이고 구성원의 유사성을 고려하면 목록에 어떤 이점이 남아 있습니까?

  1. 컨테이너가 정렬 된 상태로 유지되도록 N 개의 정수를 생성하여 컨테이너에 넣습니다. 삽입은 요소를 하나씩 읽고 새로운 큰 것을 첫 번째 큰 요소 바로 앞에 삽입하여 순진하게 수행되었습니다.
    목록을 사용하면 치수가 벡터에 비해 증가하면 시간이 지붕을 통과합니다.

  2. 컨테이너 끝에 N 정수를 삽입하십시오.
    목록과 벡터의 경우 시간이 벡터보다 3 배 빨랐지만 시간이 같은 순서로 증가했습니다.

  3. 컨테이너에 N 개의 정수를 삽입하십시오.
    타이머를 시작하십시오.
    list에 list.sort를 사용하고 벡터에 std :: sort를 사용하여 컨테이너를 정렬하십시오. 타이머를 중지하십시오.
    다시 말하지만 시간은 같은 크기만큼 증가하지만 벡터의 경우 평균 5 배 더 빠릅니다.

계속해서 테스트를 수행하고 목록이 더 나아질 몇 가지 예를 알아낼 수 있습니다.

그러나이 메시지를 읽는 당신의 공동 경험은 더 생산적인 답변을 제공 할 수 있습니다.

목록을 사용하기가 더 편리하거나 성능이 더 좋은 상황을 경험했을 수 있습니다.


2
배열 / 배열 목록에서 연결 목록을 사용하는시기를 살펴보아야 합니다. 당신이 이미하지 않은 경우
Karthik T

1
여기에 주제에 또 다른 좋은 자원이다 : stackoverflow.com/a/2209564/8360는 또한 C의 가장 ++ 내가 들어 본 지침은 특정 이유가있는 경우에만 기본 목록으로 벡터를 사용하는 것입니다.
Zachary Yates

고맙습니다. 그러나 나는 가장 좋아하는 답변에서 말하는 대부분의 내용에 동의하지 않습니다. 이러한 선입견은 대부분 실험에 의해 무효화되었습니다. 이 사람은 어떤 시험도하지 않았으며 책이나 학교에서 가르치는 광범위한 이론을 적용했습니다.
Marek Stanley

1
A는 list당신이 요소를 많이 제거하는 경우 아마도 더 나은 않습니다. vector전체 벡터가 삭제 될 때까지 의지가 시스템에 메모리를 반환 한다고 생각하지 않습니다 . 또한 테스트 # 1은 삽입 시간 만 테스트하는 것이 아닙니다. 검색과 삽입을 결합한 테스트입니다. list느린 곳에 삽입 할 곳을 찾는 것 입니다. 실제 인서트는 벡터보다 빠릅니다.
로봇 고트

3
이 질문이 (런타임) 성능, 성능 및 성능만으로 설명되는 것은 매우 일반적입니다. 이것은 많은 프로그래머들의 사각 지대 인 것 같습니다. 그들은이 측면에 초점을 맞추고 종종 훨씬 더 중요한 수십 가지 다른 측면이 있다는 것을 잊습니다.
Doc Brown

답변:


34

짧은 대답은 사례가 거의없고 사이에 있다는 것입니다. 그래도 몇 가지 있습니다.

하나는 적은 수의 큰 객체, 특히 너무 큰 객체를 저장해야 할 때 공간을 할당하는 것이 비현실적입니다. 기본적으로 벡터를 중지하거나 추가 객체를위한 공간을 할당하지 못하게하는 방법은 없습니다. 즉, 객체가 정의 된 방식입니다 (즉, 복잡한 요구 사항을 충족하기 위해 추가 공간을 할당 해야 함 ). 플랫 아웃 통해 추가 공간을 할당 할 std::list수없는 경우 요구 사항을 충족 하는 유일한 표준 컨테이너 일 수 있습니다 .

또 다른 경우는 반복기를 목록의 "관심있는"지점에 장기간 저장하고 삽입 및 / 또는 삭제를 수행 할 때는 항상 거의 모든 지점에서 반복자를 수행하는 것입니다. 이미 반복자가 있으므로 삽입 또는 삭제를 수행 할 지점까지 목록을 거치지 않습니다. 둘 이상의 스팟으로 작업하는 경우에도 똑같이 적용되지만 여전히 작업 할 가능성이있는 각 위치에 반복기를 저장하려고 계획하므로 직접 도달 할 수있는 스팟을 가장 많이 조작 할 수 있습니다. 그 지점에.

첫 번째 예는 웹 브라우저를 고려하십시오. Tab브라우저의 열린 탭에서 각 탭 객체를 나타내는 연결된 객체 목록을 유지할 수 있습니다 . 각 탭은 수십 메가 바이트의 데이터 일 수 있습니다 (특히 비디오와 같은 것이 포함 된 경우). 일반적으로 열려있는 탭의 수는 12 개 미만일 수 있으며 100은 아마도 상위 극단에 가깝습니다.

두 번째 예는 텍스트를 연결된 장 목록으로 저장하는 워드 프로세서를 고려하십시오. 각 장에는 연결된 단락 (예 : 단락) 목록이 포함될 수 있습니다. 사용자는 편집 할 때 일반적으로 편집 할 특정 지점을 찾은 다음 해당 지점 (또는 해당 단락 내부)에서 상당한 양의 작업을 수행합니다. 그렇습니다. 그들은 한 단락에서 다른 단락으로 옮겨 갈 것입니다. 그러나 대부분의 경우 이미 작동하고있는 단락이 될 것입니다.

때때로 (전역 검색 및 바꾸기와 같은) 모든 목록의 모든 항목을 살펴 보는 경우가 많지만 매우 드문 일이며, 할 때조차도 항목 내에서 충분한 작업 검색을 수행 할 것입니다. 목록을 탐색하는 시간이 거의 중요하지 않은 목록입니다.

일반적인 경우에는 첫 번째 기준에도 적합 할 수 있습니다. 장에는 상당히 적은 수의 단락이 포함되어 있으며 각 단락은 상당히 클 수 있습니다 (적어도 노드 등). 마찬가지로 비교적 적은 수의 챕터가 있으며 각 챕터는 몇 킬로바이트 정도일 수 있습니다.

즉,이 두 가지 예가 모두 약간 고안되어 있으며 링크 된 목록이 어느 쪽에도 완벽하게 작동 할 수는 있지만 어느 경우에도 큰 이점을 제공하지는 않을 것입니다. 예를 들어 두 경우 모두 일부 (빈) 웹 페이지 / 탭 또는 빈 챕터에 대해 벡터에 여분의 공간을 할당하는 것은 실제 문제가되지 않습니다.


4
+1이지만 포인터를 사용하면 첫 번째 경우가 사라집니다. 포인터는 항상 큰 개체와 함께 사용해야합니다. 연결된 목록도 두 번째 예에는 적합하지 않습니다. 배열 은 짧을 때 모든 작업에 대해 소유합니다 .
amara

2
큰 물체의 경우 전혀 작동하지 않습니다. std::vector포인터를 사용하면 연결된 모든 목록 노드 객체보다 효율적입니다.
Winston Ewert

링크 된 목록에는 여러 가지 용도가 있습니다. 동적 배열만큼 일반적이지는 않습니다. LRU 캐시는 일반적으로 연결 목록을 사용합니다.
Charles Salvia

또한 std::vector<std::unique_ptr<T>>좋은 대안이 될 수 있습니다.
중복 제거기

24

Bjarne Stroustrup 자신에 따르면 벡터는 항상 데이터 시퀀스의 기본 컬렉션이어야합니다. 요소 삽입 및 삭제를 위해 최적화하려는 경우 목록을 선택할 수 있지만 일반적으로 그렇게하지 않아야합니다. 목록의 비용은 순회 및 메모리 사용량이 느립니다.

그는 이 프리젠 테이션 에서 이에 대해 이야기 합니다 .

약 0:44에 그는 일반적으로 벡터 대리스트에 대해 이야기합니다.

소형화가 중요합니다. 벡터는 목록보다 작습니다. 예측 가능한 사용 패턴은 매우 중요합니다. 벡터를 사용하면 많은 요소를 밀어야하지만 캐시는 실제로 정말 좋습니다. ... 목록은 무작위로 접근 할 수 없습니다. 그러나 목록을 탐색 할 때 계속 임의 액세스를 수행합니다. 여기에 노드가 있고 메모리의 해당 노드로갑니다. 따라서 실제로 메모리에 무작위로 액세스하고 캐시 누락을 최대화하고 있습니다. 이는 원하는 것과 정확히 반대입니다.

약 1:08에이 문제에 대한 질문을받습니다.

우리는 일련의 요소가 필요하다는 것을 알 수 있습니다. 그리고 C ++의 기본 요소 시퀀스는 벡터입니다. 이제는 컴팩트하고 효율적이기 때문입니다. 하드웨어에 대한 구현, 매핑이 중요합니다. 삽입과 삭제를 최적화하고 싶다면 '기본 시퀀스의 버전을 원하지 않습니다. 나는 전문화 된 것을 원한다. 그리고 그렇게한다면, '저렴한 순회 및 더 많은 메모리 사용량과 같은 비용과 문제를 받아들이고 있습니다.'라고 말할만큼 충분히 알고 있어야합니다.


1
프레젠테이션에서 "0:44 및 1:08에 대해" 링크 된 내용을 간략하게 작성 하시겠습니까?
gnat

2
@gnat-확실히. 나는 개별적으로 의미가있는 것을 인용하려고 노력했으며 슬라이드의 맥락이 필요합니다.
Pete

11

내가 일반적으로 목록을 사용하는 유일한 장소는 요소를 지우고 반복자를 무효화하지 않는 곳입니다. std::vector삽입 및 삭제시 모든 반복자를 무효화합니다. std::list삽입 또는 삭제 후에 기존 요소에 대한 반복자가 여전히 유효 함을 보장합니다.


4

이미 제공된 다른 답변 외에도 목록에는 벡터에 존재하지 않는 특정 기능이 있습니다. 추가하거나 함께 병합해야하는 많은 목록이있는 경우 목록을 사용하는 것이 좋습니다.

그러나 이러한 작업을 수행 할 필요가 없다면 아마 그렇지 않을 것입니다.


3

링크 된 목록의 고유 한 캐시 / 페이지 친 화성이 부족하면 많은 C ++ 개발자가 거의 완전히 무시하고 기본 형식으로 정당화 할 수 있습니다.

연결된 목록은 여전히 ​​훌륭 할 수 있습니다

그러나 연결리스트가 될 수 있습니다 멋진 그들이 본질적으로 부족 공간적 지역성을 다시 제공하는 고정 할당에 의해 뒷받침 될 때.

예를 들어 단순히 새로운 포인터를 저장하고 포인터를 조작하여 목록을 두 개의 목록으로 나눌 수 있다는 점에서 탁월합니다. 포인터 조작만으로 노드를 한 목록에서 다른 목록으로 이동할 수 있으며 빈 목록은 단순히 단일 head포인터 의 메모리 비용을 가질 수 있습니다 .

간단한 그리드 가속기

실제 예로서, 2D 비주얼 시뮬레이션을 고려하십시오. 각 프레임에서 움직이는 수백만 개의 입자 사이의 충돌 감지와 같은 것을 가속화하는 데 사용되는 400x400 (160,000 그리드 셀)에 걸쳐있는 스크롤 화면이 있습니다 (실제로이 레벨에서 성능이 저하되는 경향이 있으므로 쿼드 트리는 피함) 동적 데이터). 모든 프레임에서 입자의 전체 무리가 끊임없이 움직입니다. 즉, 한 그리드 셀에 다른 그리드 셀에 지속적으로 존재합니다.

이 경우 각 입자가 단일 연결 목록 노드 인 경우 각 그리드 셀 head은를 가리키는 포인터 로 시작할 수 있습니다 nullptr. 새로운 파티클이 태어 났을 때, head그 셀 의 포인터가이 파티클 노드를 가리 키도록 설정하여 존재하는 그리드 셀에 넣습니다 . 입자가 한 그리드 셀에서 다음 그리드 셀로 이동할 때 포인터 만 조작하면됩니다.

이는 vectors각 그리드 셀에 160,000 을 저장 하고 프레임 단위로 항상 중간에서 뒤로 지우는 것보다 훨씬 효율적 입니다.

std :: list

이것은 고정 할당자가 뒷받침하는 수동 롤링, 방해, 단일 링크 목록을위한 것입니다. std::list는 이중 연결 목록을 나타내며 단일 포인터처럼 비어있을 때 크기가 작지 않을 수 있습니다 (공급 업체 구현에 따라 다름) std::allocator.

나는 절대로 아무 것도 사용하지 않는다는 것을 인정해야한다 list. 그러나 연결된 목록은 여전히 ​​훌륭 할 수 있습니다! 그러나 사람들이 종종 그것들을 사용하려는 유혹을받는 이유는 훌륭하지 않으며, 최소한 강제적 인 페이지 결함과 관련된 캐시 누락을 완화시키는 매우 효율적인 고정 할당자가 지원하지 않는 한 그렇게 훌륭하지 않습니다.


1
C ++ 11부터 표준 단일 링크 목록이 std::forward_list있습니다.
sharyex

2

컨테이너의 요소 크기를 고려해야합니다.

int 요소 벡터는 대부분의 데이터가 CPU 캐시에 들어가므로 매우 빠릅니다 (그리고 SIMD 명령어는 아마도 데이터 복사에 사용될 수 있습니다).

요소의 크기가 크면 테스트 1 및 3의 결과가 크게 변경 될 수 있습니다.

A로부터 매우 포괄적 인 성능 비교 :

이는 각 데이터 구조의 사용법에 대한 간단한 결론을 도출합니다.

  • 번호 크 런칭 : std::vector또는std::deque
  • 선형 검색 : std::vector또는std::deque
  • 무작위 삽입 / 제거 :
    • 작은 데이터 크기 : 사용 std::vector
    • 큰 요소 크기 : 사용 std::list(주로 검색 용이 아닌 경우)
  • 중요하지 않은 데이터 유형 : std::list특히 검색을 위해 컨테이너가 필요하지 않은 경우 사용 하십시오. 그러나 컨테이너를 여러 번 수정하면 속도가 매우 느려집니다.
  • 앞으로 밀기 : 사용 std::deque또는std::list

참고 std::deque로 매우 과소 평가 된 데이터 구조입니다.

편의상 std::list다른 요소를 삽입하고 제거 할 때 반복자가 무효화되지 않도록 보장합니다. 종종 중요한 측면입니다.


2

내 의견으로는 목록을 사용하는 가장 중요한 이유는 반복기 무효화입니다 . 벡터에 요소를 추가 / 제거하면이 벡터의 특정 요소에 대해 보유 한 모든 포인터, 참조, 반복자가 무효화되고 미묘한 버그가 발생할 수 있습니다. 또는 세그먼테이션 결함.

이것은 목록의 경우에는 해당되지 않습니다.

모든 표준 컨테이너에 대한 정확한 규칙 은이 StackOverflow 게시물에 나와 있습니다.


0

간단히 말해서 std::list<>다음 을 사용하는 이유는 없습니다 .

  • 분류되지 않은 컨테이너가 필요한 경우 std::vector<>규칙.
    요소를 벡터의 마지막 요소로 바꾸어 삭제하십시오.

  • 정렬 된 컨테이너가 필요한 경우 std::vector<shared_ptr<>>규칙.

  • 희소 색인이 필요한 경우 std::unordered_map<>규칙.

그게 다야.

링크 된 목록을 사용하는 경향이 하나 있다는 것을 알게되었습니다. 일부 추가 응용 프로그램 논리를 구현하기 위해 어떤 방식으로 연결 해야하는 기존 객체가있는 경우. 그러나이 경우 절대 사용하지 않습니다 std::list<>. 특히 대부분의 사용 사례가 선형 목록이 아닌 트리가되기 때문에 객체 내부의 (스마트) 다음 포인터에 의지합니다. 어떤 경우에는 결과 구조가 연결된 목록이고 다른 경우에는 트리 또는 방향이 지정된 비순환 그래프입니다. 이 포인터의 주요 목적은 항상 논리적 구조를 구축하고 객체를 관리하지 않는 것입니다. 우리는 std::vector<>그것을 위해있다.


-1

첫 번째 테스트에서 인서트를 어떻게 수행했는지 보여 주어야합니다. 두 번째 및 세 번째 테스트 인 벡터는 쉽게 이길 수 있습니다.

목록을 많이 사용하면 반복하는 동안 항목 제거를 지원해야합니다. 벡터가 수정되면 모든 반복자가 (잠재적으로) 유효하지 않습니다. 목록이 있으면 제거 된 요소에 대한 반복자 만 유효하지 않습니다. 다른 모든 반복자는 유효합니다.

컨테이너의 일반적인 사용 순서는 벡터, deque, list입니다. 컨테이너의 선택은 일반적으로 push_back 선택 벡터, pop_front 선택 deque, 삽입 선택 목록을 기반으로합니다.


3
반복하는 동안 항목을 제거 할 때 일반적으로 벡터를 사용하고 결과를 위해 새 벡터를 만드는 것이 좋습니다
amara

-1

내가 생각할 수있는 한 가지 요소는 벡터가 커짐에 따라 벡터가 메모리를 할당 해제하고 더 큰 블록을 반복 할당 할 때 사용 가능한 메모리가 조각화된다는 것입니다. 목록에는 문제가되지 않습니다.

이것은 push_back예약이없는 많은 수의 사본이 각 크기 조정 중에 사본을 유발하여 비효율적 이라는 사실에 추가됩니다 . 중간에 삽입하면 모든 요소가 오른쪽으로 이동하고 더 악화됩니다.

이것이 중요한 관심사인지는 모르겠지만 벡터를 피하기 위해 제 작업 (모바일 게임 개발)에서 나에게 주어진 이유였습니다.


1
아니, 벡터는 복사 할 것이고 비싸다. 그러나 (삽입 위치를 파악하기 위해) 링크 된 목록을 탐색하는 것도 비용이 많이 듭니다. 핵심은 실제로 측정하는 것입니다
Kate Gregory

@KateGregory 그 외에도 그에 따라 편집하겠습니다
Karthik T

3
그렇습니다.하지만 언급하지 않은 비용을 믿거 나 말거나 (그리고 대부분의 사람들은 그것을 믿지 않습니다), 링크 된 목록을 탐색하여 사본을 삽입 할 위치를 찾습니다 (특히 요소가 작거나 움직일 수있는 경우) 이동)) 벡터는 종종 (또는 보통) 더 빠릅니다. 믿거 나 말거나.
Kate Gregory
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.