You are here

LabVIEW 성능 및 메모리 관리 2

VI 메모리 사용

 LabVIEW에서는 사용자가 텍스트 기반 프로그래밍 언어에서 처리해야 하는 많은 상세정보를 처리해 줍니다. 텍스트 기반 언어의 주요 문제 중 하나는 메모리 사용입니다. 텍스트 기반 언어에서 프로그래머로서 여러분은 메모리를 사용하기에 앞서 메모리를 할당하고 작업이 완료되면 메모리 할당을 취소해야 합니다. 또한 사용자는 최초에 할당한 메모리의 끝을 지나서는 쓰지 않도록 주의해야 합니다. 메모리를 할당하지 않거나 충분한 메모리를 할당하지 않는 것이 텍스트 기반 언어를 사용하는 프로그래머들이 범하는 가장 큰 실수 중 하나입니다. 잘못된 메모리 할당도 디버깅하기 어려운 문제입니다. LabVIEW용 데이터-흐름 패러다임에서는 메모리 관리의 어려움을 많은 부분 해소해 줍니다. LabVIEW에서는 변수를 할당하거나 값을 변수 사이에 지정하지 않아도 됩니다. 대신 데이터의 전환을 나타내는 연결이 있는 블록다이어그램을 생성합니다.
데이터를 생성한 함수는 해당 데이터에 대한 스토리지 할당을 처리합니다. 데이터가 더 이상 사용되지 않으면 관련 메모리는 할당 취소됩니다. 새 정보를 배열이나 문자열에 추가하면 새 정보를 관리할 수 있도록 충분한 메모리가 자동으로 할당됩니다. 이 자동 메모리 처리는 LabVIEW가 제공하는 주요 혜택 중 하나입니다. 그러나 자동으로 이루어지기 때문에 제어력은 떨어집니다. 프로그램에서 큰 데이터 세트를 사용하여 작업하는 경우에는 메모리 할당이 언제 일어날 지에 대해 어느 정도 이해를 갖추는 것이 중요합니다. 수반된 원칙을 이해하면 프로그램의 메모리 요구사항이 훨씬 작아지게 됩니다. 또한 메모리 할당과 데이터 복사에 상당한 시간이 소요될 수 있으므로 메모리 사용을 최소화하는 방법을 이해하면 VI 실행 속도를 향상시키는 데도 도움이 됩니다.

 

버추얼 메모리

메모리 양이 제한되어 있는 머신을 사용하는 경우, 어플리케이션에 사용 가능한 메모리 양을 늘리기 위해 버추얼 메모리 사용을 고려해 볼 수 있을 것입니다. 버추얼 메모리는 RAM 스토리지를 위해 가용 디스크 공간을 사용하는 운영 체제의 기능입니다. 큰 버추얼 메모리를 할당하면 어플리케이션에서는 이를 일반적으로 스토리지에 사용 가능한 메모리로 인식합니다.
어플리케이션에서는 메모리가 실제 RAM인지 버추얼 메모리인지는 문제가 되지 않습니다. 운영 체제는 메모리가 버추얼이라는 사실을 숨깁니다. 주요 차이는 속도입니다. 버추얼 메모리를 사용할 경우 메모리가 운영 체제에서 디스크와 스왑핑될 때 때때로 성능이 매우 느려지게 됩니다. 버추얼 메모리는 대규모 어플리케이션을 실행할 수 있도록 도와주지만 중요한 시간적 제약이 있는 어플리케이션에는 적합하지 않을 수 있습니다.

 

Macintosh 메모리

Macintosh에서 어플리케이션을 실행하면 시스템에서는 단일 메모리 블록을 할당하며 여기서 모든 메모리가 할당됩니다. VI를 로드하면 VI의 컴포넌트가 해당 메모리로 로드됩니다. 이와 비슷하게 VI를 실행하면 그 VI에서 조작하는 모든 메모리가 그 단일 메모리 블록 밖에 할당됩니다.
파인더에서 File»Get Info 명령을 사용하여 시작 시 시스템이 할당하는 메모리의 양을 구성합니다. 어플리케이션에서 메모리가 부족해 지더라도 이 메모리 풀의 크기를 늘릴 수 없습니다. 그러므로 이 매개변수를 가능한 실용적일 수 있도록 크게 설정합니다. 16MB 머신을 사용하는 경우 병렬로 실행하고자 하는 어플리케이션을 고려해야 합니다. 다른 어플리케이션을 실행할 계획이 없으면 메모리 기본 설정을 가능한 크게 설정합니다.

 

VI 컴포넌트 메모리 관리

VI에는 다음 네 가지 주요 컴포넌트가 있습니다.

  • 프런트패널
  • 블록다이어그램
  • 코드(머신 코드로 컴파일된 다이어그램)
  • 데이터(컨트롤 및 인디케이터 값, 기본 데이터, 다이어그램 상수 데이터 등)

 

VI가 프런트패널, 코드(플랫폼과 일치하는 경우)를 로드하면 VI용 데이터가 메모리로 로드됩니다. 플랫폼 변경이나 SubVI 인터페이스의 변경으로 인해 VI를 재컴파일해야 할 경우 블록다이어그램도 메모리로 로드됩니다. VI는 SubVI의 코드 및 데이터 공간도 메모리로 로드합니다. 특정한 상황에서는 일부 SubVI의 프런트패널도 역시 메모리로 로드됩니다. 예를 들어, SubVI에서 속성 노드를 사용하는 경우 속성 노드는 프런트패널 컨트롤의 상태 정보를 조작하기 때문에 이 때 SubVI의 프런트패널도 로드될 수 있습니다. SubVI 및 패널에 대한 자세한 내용은 이 어플리케이션 노트의 뒷부분에서 설명합니다. VI 컴포넌트 구성에서 중요한 요점은 VI의 섹션을 SubVI로 변환할 때 일반적으로 많은 메모리를 사용하지 않는다는 것입니다. SubVI가 없는 큰 단일 VI를 생성할 경우 해당 최상위 VI에 대한 프런트패널, 코드 및 데이터가 메모리에 있게 됩니다. VI를 SubVI로 분할하면 최상위 VI의 코드가 작아지고 SubVI의 코드 및 데이터는 메모리에 상주하게 됩니다. 어떤 경우 런타임 메모리 사용이 낮을 수도 있습니다. 이에 대해서는 이 어플리케이션 노트의 메모리 사용 모니터링 섹션에서 설명하도록 합니다.

 

또한 대규모 VI는 편집하는 데 시간이 더 걸린다는 것도 알게 될 것입니다. 편집기에서는 작은 VI를 보다 효율적으로 처리할 수 있기 때문에 VI를 SubVI로 분할하면 이 문제를 피할 수 있습니다. 또한 보다 계층적인 VI 조직이 일반적으로 관리 및 읽기가 더 편리합니다.

주: 특정 VI의 프런트패널이나 블록다이어그램이 화면보다 훨씬 크면 액세스하기 쉽도록 SubVI로 분할하려고 할 것입니다.

 

데이터-흐름 프로그래밍 및 데이터 버퍼

데이터-흐름 프로그래밍에서는 일반적으로 변수를 사용하지 않습니다. 데이터-흐름 모델에서는 대개 노드를 데이터 입력을 소모하여 데이터 출력을 산출하는 것으로 설명합니다. 이 모델을 리터럴하게 구현하면 매우 많은 메모리 양을 사용할 수 있지만 성능이 느려지는 어플리케이션이 됩니다. 모든 함수는 출력이 전달되는 모든 목적지에 대한 데이터 사본을 생성합니다. LabVIEW 컴파일러는 언제 메모리가 재사용될 수 있는지 파악하려 시도하고 출력의 목적지를 살펴보고 각 개별 터미널을 위한 사본을 만들어야 하는지 여부를 파악함으로써 이러한 구현을 향상시킵니다.
예를 들어, 컴파일러에 대한 보다 전통적인 접근법에서와 같이 다음 블록다이어그램에서는 2개의 데이터 메모리 블록(각각 입력용 및 출력용)을 사용합니다.

 

 

 

입력 배열과 출력 배열에는 동일한 수의 원소가 들어 있으며 두 배열의 데이터 타입은 동일합니다. 들어오는 배열을 데이터 버퍼라고 생각합니다. 출력을 위한 새로운 버퍼를 생성하는 대신 컴파일러에서는 입력 버퍼를 재사용합니다. 그러면 메모리 할당이 런타임 시 이루어지지 않아도 되기 때문에 메모리가 절약되고 실행 속도가 향상됩니다.
그러나 컴파일러는 다음 그림에 설명되어 있는 경우와 같이 모든 경우에 메모리 버퍼를 재사용할 수 있는 것은 아닙니다.

 

 

신호는 데이터의 단일 소스를 통과하여 여러 목적지로 갑니다. Replace Array Element 함수는 입력 배열을 수정하여 출력 배열을 생성합니다. 이 경우 컴파일러는 2개의 함수를 위한 새로운 데이터 버퍼를 생성하고 배열 데이터를 버퍼로 복사합니다. 그러므로 함수 주 하나는 입력 배열을 재사용하고 나머지는 그렇지 않습니다. 이 블록다이어그램은 약 12KB(원래 배열에 4 KB, 여분의 2개 데이터 버퍼 각각에 4 KB씩)를 사용합니다.

이제 다음의 블록다이어그램을 검토해 봅시다.

  

앞에서와 같이 입력은 세 함수로 곁가지가 나 있습니다. 그러나 이 경우 Index Array 함수는 입력 배열을 수정하지 않습니다. 데이터를 여러 위치로 전달하면 그 위치에서는 모두 그 데이터를 수정하지 않고 읽게 되며 LabVIEW에서는 데이터 사본을 만들지 않습니다. 이 블록다이어그램은 약 4KB의 메모리를 사용합니다.

마지막으로 다음 블록다이어그램을 살펴 봅시다.

 

이 경우 입력은 둘 중 하나에서 데이터를 수정하는 두 함수로 곁가지가 나 있습니다. 두 함수간에는 아무런 의존성이 없습니다. 그러므로 Replace Array Element 함수에서 안전하게 데이터를 수정할 수 있도록 최소 하나의 사본을 만들어야 함을 예측할 수 있습니다. 그러나 이 경우 컴파일러는 데이터를 읽는 함수가 먼저 실행되고 데이터를 수정하는 함수는 나중에 실행되도록 함수 실행을 스케줄링합니다. 이런 식으로 Replace Array Element 함수는 복제 배열을 생성하지 않고도 들어오는 배열 버퍼를 재사용하는 것입니다. 노드 순서가 중요한 경우 시퀀스나 다른 입력에 대한 한 노드의 출력을 사용하여 순서지정을 명시적이게 합니다.

실제로, 컴파일러로 블록다이어그램을 분석하는 것은 완벽하지 않습니다. 몇몇 경우에서 컴파일러는 블록다이어그램 메모리를 재사용하기 위핸 최적의 방법을 결정하지 못할 수도 있습니다.

 

메모리 사용 모니터링

다음과 같이 메모리 사용량을 결정하는 방법이 몇 가지 있습니다.

 

Help»About LabVIEW를 선택하여 어플리케이션에서 사용한 총 메모리 양을 요약하는 통계를 봅니다. 이 통계에는 VI의 메모리 뿐 아니라 어플리케이션에서 사용하는 메모리도 포함됩니다. VI 세트를 실행하기 전과 실행한 후에 이 메모리 양을 확인하여 VI에서 메모리를 얼마나 사용하고 있는지 대략적으로 파악할 수 있습니다.

성능 프로파일러를 사용하면 VI의 다이내믹 메모리 사용에 대한 보기를 얻을 수 있습니다. 성능 프로파일러는 실행 당 각 VI에서 사용한 최소, 최대 및 평균 바이트 수와 블록 수에 대한 통계를 저장합니다. 자세한 내용은 이 어플리케이션 노트의 시작 부분에 나오는 VI 성능 프로파일링 섹션을 참조하십시오.

다음 그림에 나와 있듯이 Windows»Show VI Info를 사용하면 VI의 메모리 사용량에 대한 분석도를 볼 수 있습니다. 왼쪽 컬럼은 디스크 사용량에 대해 요약되어 있으며 오른쪽 컬럼은 현재 VI의 다양한 컴포넌트에 RAM이 얼마만큼 사용되고 있는지 요약되어 있습니다. 이 통계에는 SubVI의 메모리 사용량은 포함되어 있지 않습니다.

 

  메모리 사용량을 파악하는 네 번째 방법은 메모리 모니터 VI(예제 디렉토리의 memmon.llb에 있음)를 사용하는 것입니다. 이 VI는 VI Server 함수를 사용하여 메모리에 있는 모든 VI의 메모리 사용량을 결정합니다.

 

더 나은 메모리 사용을 위한 규칙

이전 논의의 주요 요점은 컴파일러에서 메모리를 지능적으로 재사용하려 시도한다는 것이었습니다. 컴파일러에서 메모리를 재사용할 수 있는 시점과 그렇지 않은 시점을 구분하는 규칙은 상당히 복잡하기 때문에 이 어플리케이션 노트의 마지막에서 설명하도록 합니다. 실제로 다음 규칙이 메모리를 효율적으로 사용하는 VI를 생성하는 데 도움이 될 수 있을 것입니다.

  • VI를 SubVI로 분할하면 일반적으로 메모리 사용이 손상을 입지 않습니다. 많은 경우에 실행 시스템에서 SubVI가 실행 중이 아닐 때 SubVI 데이터를 요구할 수 있기 때문에 메모리 사용이 향상됩니다.  
  • 스칼라 값 사본에 대해서는 너무 많이 걱정하지 마십시오. 메모리 사용에 부정적인 영향을 미치려면 많은 스칼라가 있어야 합니다.
  • 배열 또는 문자열을 가지고 작업할 때 글로벌 변수와 로컬 변수를 많이 사용하지 마십시오. 글로벌 변수나 로컬 변수를 읽으면 변수 데이터의 사본이 생성됩니다.
  • 필요한 경우가 아니면 큰 배열 및 문자열을 열린 프런트패널에 표시하지 마십시오. 열린 프런트패널의 인디케이터는 표시한 데이터의 사본을 유지합니다.
  • SubVI의 프런트패널이 표시되지 않는 경우 SubVI에 사용되지 않은 속성 노드를 남겨두지 마십시오. 속성 노드는 SubVI의 프런트패널이 메모리에 남아있게 하므로 불필요하게 메모리가 사용될 수 있습니다.
  • 블록다이어그램을 설계할 때 입력 크기가 출력 크기와 다른 곳을 주의합니다. 예를 들어, Build Array 또는 Concatenate Strings 함수를 사용하여 배열이나 문자열의 크기를 자주 증가시키는 곳은 데이터 사본을 생성하고 있는 것입니다.  
  • 데이터를 SubVI 및 함수로 전달할 때 배열에 일관적인 데이터 타입을 사용하고 강제 형변환 점에 주의합니다. 데이터 타입을 변경하면 실행 시스템에서는 데이터의 사본을 만듭니다.  
  • 클러스터 또는 큰 배열이나 문자열을 포함하는 클러스터 배열과 같은 복잡하고 계층적인 데이터 타입을 사용하지 마십시오. 더 많은 메모리를 사용하게 될 것입니다.  자세한 내용과 데이터 타입 설계 전략에 대한 제안은 이 어플리케이션 노트의 뒤쪽에 나오는 효율적인 데이터 구조 개발 섹션을 참조하십시오.

 

프런트패널의 메모리 이슈

프런트패널이 열려 있으면 컨트롤과 인디케이터는 표시한 데이터의 자체적인 개인 사본을 보유합니다.
다음 그림은 프런트패널 컨트롤 및 인디케이터를 추가한 증가 함수를 나타냅니다.

 

 VI를 실행할 때 프런트패널 컨트롤의 데이터는 블록다이어그램으로 전달됩니다. 증가 함수는 입력 버퍼를 재사용합니다. 그리고 나면 인디케이터가 표시 목적으로 데이터의 사본을 만듭니다. 그러므로 버퍼의 사본은 3개가 됩니다.
이러한 프런트패널 컨트롤의 데이터 보호를 통해 컨트롤에 데이터를 입력하고 연관된 VI를 실행하며 데이터가 후속 노드로 전달될 때 데이터 변경을 보는 경우가 생기지 않게 됩니다. 이와 비슷하게 데이터는 인디케이터의 경우 보호되므로 새 데이터를 받기 전까지는 이전 내용을 신뢰할 수 있게 표시할 수 있습니다.

 

SubVI가 있으면 컨트롤 및 인디케이터를 입력과 출력으로 사용할 수 있습니다. 실행 시스템은 다음과 같은 상황에서 SubVI의 컨트롤 및 인디케이터 데이터의 사본을 만듭니다.

  • 프런트패널이 메모리에 있을 경우. 이 같은 상황은 다음과 같은 이유로 인해 발생할 수 있습니다.
  • 프런트패널이 열려 있는 경우.
  • VI가 변경되었으나 저장되지 않은 경우(VI가 저장될 때까지는 VI의 모든 컴포넌트가 메모리에 남이 있음).
  • 패널에서 데이터 인쇄를 사용하는 경우.
  • 블록다이어그램에서 속성 노드를 사용하는 경우.
  • VI가 로컬 변수를 사용하는 경우.
  • 패널이 데이터로깅을 사용하는 경우.
  • 컨트롤이 일시중단 데이터 범위 확인을 사용하는 경우.

 

속성 노드가 패널이 닫힌 SubVI에서 차트 내역을 읽으려면 컨트롤 또는 인디케이터에서 전달된 데이터를 표시해야 합니다. 이와 같이 다른 속성이 많이 있기 때문에 실행 시스템은 SubVI가 속성 노드를 사용하는 경우 메모리에 SubVI 패널을 보존합니다. 프런트패널에서 프런트패널 데이터로깅이나 데이터 인쇄를 사용할 경우 컨트롤 및 인디케이터는 데이터의 사본을 유지합니다. 또한 패널을 인쇄할 수 있도록 데이터 인쇄를 위해 메모리에 패널이 보존됩니다.
VI Setup이나 SubVI Setup을 통해 호출되었을 때 SubVI에서 프런트패널을 표시하도록 설정하면 SubVI가 호출될 때 패널이 메모리로 로드된다는 사실을 기억하십시오. Close if Originally Closed 항목을 설정한 경우 SubVI가 실행을 끝내면 패널이 메모리에서 제거됩니다.

 

SubVI는 데이터 메모리를 재사용할 수 있습니다
일반적으로 SubVI는 SubVI의 블록다이어그램이 최상위에서 복제되는 것처럼 쉽게 호출자로부터 데이터 버퍼를 사용할 수 있습니다. 대부분의 경우 블록다이어그램의 섹션을 SubVI로 변환하는 경우 더 많은 메모리를 사용하지 않습니다. 앞의 섹션에서 설명하였듯이 특수 디스플레이 요구사항이 있는 VI의 경우 프런트패널 및 컨트롤을 위한 추가적인 메모리 사용이 있을 수 있습니다.

 

메모리가 할당 취소되는 시점의 이해

다음 블록다이어그램을 살펴봅시다.

 

  Mean VI가 실행된 후에는 데이터 배열이 더 이상 필요하지 않습니다. 데이터가 더 이상 필요하지 않게 되는 시점을 결정하는 것은 큰 블록다이어그램에서는 훨씬 복잡해지기 때문에 실행에서는 실행하는 동안 특정 VI의 데이터 버퍼를 할당 취소하지 않습니다.
Macintosh에서 실행 시스템의 메모리가 적은 경우 현재 실행되고 있지 않은 모든 VI가 사용하는 데이터 버퍼를 할당 취소합니다. 실행 시스템은 프런트패널 컨트롤, 인디케이터, 글로벌 변수 또는 초기화되지 않은 시프트 레지스터에서 사용하는 메모리는 할당 취소하지 않습니다. 이제 동일한 VI를 큰 VI의 SubVI로 간주해 봅시다. 데이터 배열은 SubVI에서만 생성되어 사용됩니다. Macintosh에서 SubVI가 실행되지 않고 시스템의 메모리가 적은 경우 SubVI의 데이터를 할당 취소할 수도 있습니다. 이러한 경우, SubVI를 사용하면 메모리 사용을 줄일 수 있습니다.
Windows 및 Unix 플랫폼에서는 VI가 닫히고 메모리에서 제거되기 않는 한, 데이터 버퍼가 일반적으로 할당 취소되지 않습니다. 메모리는 필요에 따라 운영 체제로부터 할당되며 버추얼 메모리는 이들 플랫폼에서 훌륭히 작동합니다. 단편화로 인해 어플리케이션이 실제보다 훨씬 많은 메모리를 사용하는 것처럼 보일 수 있습니다. 메모리가 할당되고 다시 확보됨에 따라 어플리케이션에서는 사용되지 않은 블록을 운영 체제로 반환할 수 있도록 메모리 사용을 통합하려 합니다.

모든 플랫폼에서 선택적으로 “Deallocate memory as soon as possible” 기본 설정을 선택할 수 있습니다. 이 항목이 켜지면 SubVI는 실행이 완료된 후 즉시 메모리를 할당 취소합니다. 이는 메모리 사용에는 도움이 될 수 있으나 성능을 상당히 저하시키게 됩니다.

 

출력이 입력 버퍼를 재사용할 수 있는 시점 결정

출력이 입력과 크기 및 데이터 타입이 같고, 입력이 필요하지 않은 경우 출력은 입력 버퍼를 재사용할 수 있습니다. 앞서 언급하였듯이 입력이 다른 곳에서 사용되는 일부 경우 컴파일러와 실행 시스템은 입력을 출력 버퍼에 재사용할 수 있는 방식으로 코드 실행 순서를 정렬할 수 있습니다. 그러나 이에 대한 규칙은 복잡하므로 여기에만 의존하지 마십시오.

 

일관적인 데이터 타입

입력이 출력과 데이터 타입이 다르면 출력은 그 입력을 재사용할 수 없습니다. 예를 들어, 16비트 정수에 32비트 정수를 추가하면 16비트 정수가 32비트 정수로 변환되고 있음을 보여주는 강제 형변환 점이 나타납니다. 32비트 정수 입력은 다른 요구사항(예를 들어, 32비트 정수가 다른 곳에서 재사용되고 있지 않음) 을 모두 만족시킨다는 가정 하에 출력 버퍼에 사용 가능합니다.
또한, SubVI의 강제 형변환 점과 많은 함수는 데이터 타입의 변환을 의미하기도 합니다. 일반적으로 컴파일러는 변환된 데이터용으로 새로운 버퍼를 생성합니다. 메모리 사용을 최소화하려면 가능한 일관적인 데이터 타입을 사용합니다. 그러면 데이터 크기가 향상되기 때문에 데이터 사본 수가 줄어듭니다. 일관적인 데이터 타입을 사용하면 컴파일러에서 데이터 버퍼를 재사용할 수 있는 시점을 결정할 때 더 유연해집니다.
일부 어플리케이션에서는 더 작은 데이터 타입 사용을 고려할 수 있습니다. 예를 들어, 8바이트 배정도 숫자 대신 4바이트 단정도 숫자의 사용을 생각해 볼 수 있습니다. 그러나 불필요한 변환은 피해야 하므로 호출 가능한 SubVI에서 어떤 데이터 타입을 예상하는지 주의 깊게 고려해 봐야 합니다.

 

적합한 타입의 데이터를 생성하는 방법

1000개의 임의 값 배열이 생성되어 스칼라에 추가되는 다음의 예를 참조해 봅시다. Add 함수의 강제 형변환 점은 스칼라는 단정도(single-precision)이지만 임의 데이터가 배정도(double-precision)이기 때문에 발생합니다. 스칼라는 추가되기 전에 배정도로 향상됩니다. 그리고 나서 결과 데이터가 인디케이터로 전달됩니다. 이 블록다이어그램은 16KB의 메모리를 사용합니다.

 

다음 그림은 배정도 임의 값 배열을 단정도 임의 값으로 변환하여 이 문제를 정정하고자 하는 잘못된 시도를 보여주고 있습니다. 이 예에서는 앞의 예와 동일한 양의 메모리를 사용합니다.

 

 다음 그림에 나와 있듯이 최고의 솔루션은 임의 값이 생성되고 배열을 생성하기 전에 이를 단정도로 변환하는 것입니다. 그러면 한 데이터 타입에서 다른 타입으로 대용량 데이터 버퍼를 변환하지 않아도 됩니다.

데이터 크기 조절의 지속적 예방

출력 크기가 입력 크기와 다르면 출력은 입력 데이터 버퍼를 재사용하지 않습니다. 배열이나 문자열 크기를 늘리거나 줄이는 Build Array, String Concatenate 및 Array Subset과 같은 함수의 경우 그렇습니다. 배열 및 문자열을 사용하여 작업하는 경우 이 함수를 계속해서 사용하지 않도록 합니다. 왜냐하면 프로그램은 계속해서 데이터를 복사하고 있기 때문에 더 많은 데이터 메모리를 사용하고 더욱 느리게 실행되기 때문입니다.

예 1 ‐ 배열 작성

데이터 배열을 생성하는 데 사용되는 다음 블록다이어그램을 살펴 봅시다. 이 블록다이어그램에서는 계속해서 Build Array를 호출하여 새 원소를 연결함으로써 루프에 배열을 생성합니다. 입력 배열은 Build Array에서 재사용됩니다. VI는 반복횟수마다 버퍼의 크기를 조절하여 새 배열을 위한 공간을 만들고 새 원소를 추가합니다. 그 결과 실행 속도는 느려지는데 특히 루프가 수 차례 실행되는 경우는 더 그렇습니다.

 

 
루프의 각 반복횟수마다 배열에 값을 추가하려는 경우 루프 에지에 오토인덱싱을 사용하면 최상의 성능을 볼 수 있습니다. For 루프를 사용하는 경우 VI는 배열 크기를 사전 결정하고(N에 연결된 값에 따라) 버퍼의 크기를 한 번만 조절합니다.

 

 

While 루프를 사용하는 경우 배열의 끝 크기를 알 수 없으므로 오토인덱싱은 그다지 효율적이지 않습니다. 그러나 While 루프 오토인덱싱으로 인해 큰 증분 단위로 출력 배열 크기를 증가시킴으로써 매 반복횟수마다 출력 배열의 크기를 조절하지 않아도 됩니다. 루프가 완료되면 출력 배열은 정확한 크기로 크기 조절됩니다. While 루프 오토인덱싱의 성능은 For 루프 오토인덱싱 성능과 거의 동일합니다. 

 

 

오토인덱싱에서는 루프의 각 반복횟수마다 사용자가 결과 배열에 값을 추가할 것이라 가정합니다. 배열에 조건부로 값을 추가해야 하지만 배열 크기 상한을 결정할 수 있는 경우 배열의 사전 할당과 Replace Array Element를 통해 배열을 채우는 것을 생각해 볼 수 있습니다.
배열 값 채우기를 완료하면 배열을 정확한 크기로 크기 조절할 수 있습니다. 배열은 한 번만 생성되며 Replace Array Element는 출력 버퍼에 입력 버퍼를 재사용할 수 있습니다. 이 원소의 성능은 오토인덱싱을 사용하는 루프의 성능과 매우 비슷합니다. 이 기법을 사용하는 경우 Replace Array Element는 배열의 크기를 조절하지 않으므로 값을 교체하는 배열이 결과 데이터를 수용할 수 있을 만큼 충분히 큰지 주의해야 합니다.

이 프로세스의 예가 아래 그림에 나와 있습니다.

 

예 2 ‐ 문자열 검색

Match Pattern 함수를 사용하여 패턴의 문자열을 검색할 수 있습니다. 사용하는 방식에 따라 불필요하게 문자열 데이터 버퍼를 생성하면 성능이 느려질 수도 있습니다. 다음 그림은 Match Pattern용 Help 창을 나타냅니다.

 

 

문자열의 정수와 일치시키려 한다고 가정하면 [0-9]+를 이 함수에 대한 정규 식 입력으로 사용할 수 있습니다. 문자열의 모든 정수 배열을 생성하려면 루프를 사용하고 반환되는 오프셋 값이 ‐1이 될 때까지 Match Pattern을 반복해서 호출합니다.
다음 블록다이어그램은 문자열의 모든 정수를 스캐닝하는 한 가지 방법입니다. 여기서는 빈 배열을 생성한 후 루프의 남아 있는 문자열에서 각 반복횟수마다 숫자 패턴을 검색합니다. 패턴을 찾으면(오프셋이 ‐1이 아님) 이 블록다이어그램에서는 Build Array를 사용하여 숫자를 결과로 생긴 수 배열에 추가합니다. 문자열에 남아 있는 값이 없으면 Match Pattern은 ‐1을 반환하고 블록다이어그램은 실행을 완료합니다.

 

이 블록다이어그램의 한 가지 문제점은 루프에서 Build Array를 사용하여 새 값을 이전 값에 연결한다는 것입니다. 이 방법 대신 오토인덱싱을 사용하여 루프의 에지에 값을 축적할 수 있습니다. Match Pattern에서 일치하는 항목을 찾지 못한 루프의 마지막 반복횟수에서는 배열에 여분의 원치 않는 값이 생기게 됨을 알아야 합니다. 이에 대한 한 가지 솔루션은 Array Subset을 사용하여 여분의 원치 않는 값을 제거하는 것입니다. 이에 대해서는 다음 그림에 나와 있습니다.

 

 

이 블록다이어그램의 다른 문제는 루프를 통과할 때마다 매 번 남아 있는 문자열의 불필요한 사본을 생성한다는 것입니다. Match Pattern에는 검색을 시작할 지점을 표시하는 데 사용할 수 있는 입력이 있습니다. 이전 반복횟수의 오프셋을 기억할 경우 이 숫자를 사용하여 다음 반복횟수에서 검색을 시작할 지점을 표시할 수 있습니다. 이 기법은 다음 그림에 나와 있습니다. 

 

 

효율적인 데이터 구조 개발

이전 섹션에서 설명했던 요점 중 하나는 클러스터나 큰 배열이나 문자열을 포함하는 클러스터 배열과 같은 계층적 데이터 구조는 효율적으로 조작될 수 없다는 사실입니다. 이 섹션에서는 왜 그런지에 대해 설명하고 보다 효율적인 데이터 타입을 선택하는 전략에 대해 제시합니다. 복잡한 데이터 구조가 가지는 문제는 액세스하는 원소의 사본이 생성되도록 하지 않고는 데이터 구조의 원소에 액세스하여 변경하기 어렵다는 것입니다. 원소 자체가 배열이나 문자열인 경우와 같이 큰 원소인 경우 이 추가 사본에서 메모리를 복사하는 데 더 큰 메모리를 사용하며 시간도 더 걸립니다.
일반적으로 스칼라 데이터 타입은 매우 효율적으로 조작할 수 있습니다. 유사하게 원소가 스칼라인 작은 문자열 및 배열도 효율적으로 조작할 수 있습니다. 스칼라 배열의 경우 다음 코드는 배열의 값을 증가시키기 위해 수행하는 작업을 보여줍니다. 

 

 

 

전체 배열의 여분의 사본을 생성하는 것을 불필요하므로 작업을 매우 효율적입니다. 또한 Index Array 함수에 의해 생성된 원소는 효율적으로 생성하여 조작 가능한 스칼라입니다.
클러스터에 스칼라만 들어있다고 가정할 경우 이러한 사실은 클러스터 배열에도 동일하게 적용됩니다. 다음 블록다이어그램에서는 Unbundle 및 Bundle을 사용해야 하므로 원소의 조작이 조금 더 복잡해집니다. 그러나 클러스터는 작으므로(스칼라는 매우 작은 메모리 사용)클러스터 원소에 액세스하여 원소를 원래 클러스터로 다시 교체하는 작업에는 큰 오버헤드가 수반되지 않습니다.

다음 그림은 번들링 해제, 작동 및 재번들링의 효율적인 패턴을 보여줍니다. 데이터 소스에서 나온 와이어에는 Unbundle 함수 입력 및 Bundle 함수의 중간 터미널과 같이 목적지가 2개만 있어야 합니다. LabVIEW는 이 패턴을 인식하여 성능이 우수한 모드를 생성할 수 있습니다.

 

 

 

각 클러스터에 큰 부분 배열이나 문자열이 있는 클러스터 배열이 있으면 클러스터의 원소 값을 인덱싱하여 변경하면 메모리 및 속도에 있어서는 비용이 더 들 수 있습니다.

전체 배열에서 원소를 인덱싱하면 그 원소의 사본이 생성됩니다. 그러므로 클러스터 및 이에 상응하는 큰 부분 배열 또는 문자열의 사본이 만들어집니다. 문자열 및 배열은 가변 크기이므로 복사 프로세스에는 문자열 또는 부분 배열의 데이터를 실제로 복사하는 오버헤드 뿐 아니라 적정 크기의 문자열 또는 부분 배열을 만드는 메모리 할당 호출이 수반될 수 있습니다. 이 작업을 몇 번만 수행하려고 계획한 경우에는 중요하지 않을 수 있습니다. 그러나 어플리케이션이 이 데이터 구조에 빈번히 액세스하는 데 집중하는 경우, 메모리 및 실행 오버헤드가 빠르게 증가할 수 있습니다.

솔루션은 데이터의 대안 표시(alternative representations)를 살펴보는 것입니다. 다음 3가지 사례 연구에서는 3가지의 서로 다른 어플리케이션과 각 사례의 최상의 데이터 구조에 대한 제안을 제시하고 있습니다.

 

사례 연구 1 ‐ 복잡한 데이터 타입 피하기

여러 테스트의 결과를 기록하고자 하는 어플리케이션에 대해 생각해 봅시다. 결과에서는 테스트를 설명하는 문자열과 테스트 결과의 배열을 필요로 할 것입니다. 이 정보를 저장하는 데 사용을 고려해 볼 수 있는 한 가지 데이터 타입이 다음 그림에 나와 있습니다.

 

 

배열의 원소를 변경하려면 전체 배열의 원소를 인덱싱해야 합니다. 이제 그 클러스터에서 원소를 번들링 해제하여 배열에 도달해야 합니다. 그리고 나서 배열의 원소를 교체하고 결과 배열을 클러스터에 저장합니다. 마지막으로 결과 클러스터를 원래 배열에 저장하면 됩니다. 이에 대한 예는 다음 그림에 나와 있습니다.

 


번들링 해제/인덱싱의 각각의 레벨에서는 생성되는 데이터의 사본이 만들어질 수 있습니다. 사본은 꼭 생성하지 않아도 되는 것입니다. 데이터를 복사하면 시간과 메모리 측면에서 모두 비용이 듭니다. 이에 대한 솔루션은 데이터 구조를 가능한 flat하게 만드는 것입니다. 예를 들어, 이 사례 연구에서는 데이터 구조를 2개의 배열로 분할합니다. 첫 번째 배열은 문자열의 배열이고 두 번째 배열은 각 로우가 주어진 테스트의 결과가 되는 2D 배열입니다. 이 결과는 다음 그림에 나와 있습니다.

 

 

이 데이터 구조에서는 다음 그림에서와 같이 Replace Array Element 함수를 사용하여 배열 원소를 직접 교체할 수 있습니다.

 

 

사례 연구 2 ‐ 혼합 데이터 타입의 글로벌 테이블

여기에 정보 테이블을 유지하고자 하는 다른 어플리케이션이 나와 있습니다. 이 어플리케이션에서는 데이터를 전역으로 액세스가능하도록 하고자 합니다. 이 테이블에는 게인을 포함하여 인스트루먼트의 설정, 전압 하한 및 상한 그리고 채널을 나타내는 이름이 포함될 수 있습니다.
어플리케이션 전체에서 데이터에 액세스 가능하게 하려면 다음의 SubVI, Change Channel Info VI 및 Remove Channel Info VI와 같이 SubVI 세트를 생성하여 테이블의 데이터에 액세스하도록 하는 방법을 고려할 수 있습니다.

 

 

다음 섹션에서는 이 VI에 대한 3가지 구현 방법을 제시합니다.

Obvious Implementation

이 함수 세트를 사용할 경우 원본으로 사용하는 테이블에 고려해야 할 데이터 구조가 여러 개 있습니다. 먼저 각 클러스터에 게인, 하한, 상한 및 채널 이름이 들어 있는 클러스터 배열을 포함하는 글로벌 변수를 사용할 수 있습니다.
앞의 섹션에서 설명하였듯이 이러한 데이터 구조는 일반적으로 데이터에 액세스하려면 여러 레벨의 인덱싱 및 번들링 해제를 거쳐야 하므로 효율적으로 관리하기가 어렵습니다. 또한 데이터 구조는 여러 정보의 조각이 응집되어 있는 것이므로 채널을 검색하는 데 Search 1D Array 함수를 사용할 수 없습니다. 클러스터 배열에서 특정 클러스터를 검색하려면 Search 1D Array를 사용할 수는 있지만 단일 클러스터 원소와 일치하는 원소를 검색하는 데는 사용할 수 없습니다.

 

대안 1

앞의 예에서와 같이 2개의 별도의 배열에 데이터를 보존하도록 선택합니다. 한 배열에는 채널 이름이 들어 있고 다른 배열에는 채널 데이터가 들어 있습니다. 이름 배열의 특정 채널 이름의 인덱스를 사용하여 다른 배열에서 상응하는 채널 데이터를 찾습니다.
문자열 배열은 데이터와는 별개이기 때문에 Search 1D Array 함수를 사용하여 채널을 검색할 수 있다는 점에 주의하십시오.
실제로 Change Channel Info VI를 사용하여 1,000개의 채널 배열을 생성하는 경우 이 구현은 이전 버전보다 대략 2배 가량 빠릅니다. 이러한 변화는 성능에 영향을 미치는 다른 오버헤드가 있기 때문에 그다지 중요하지 않습니다. 이 어플리케이션 노트의 메모리 사용 섹션에 나온 주에서는 로컬 변수 및 글로벌 변수의 과다한 사용에 대해 경고합니다. 글로벌 변수를 읽으면 글로벌 변수 데이터의 사본이 생성됩니다. 그러므로 배열 데이터의 완벽한 사본은 원소에 액세스할 때마다 생성되는 것입니다. 다음 방법에서는 이러한 오버헤드를 피할 수 있는 보다 효율적인 방법을 보여줍니다.

 

대안 2

글로벌 데이터를 저장하는 대체 방법이 있는데 이는 초기화되지 않은 시프트 레지스터를 사용하는 것입니다. 본질적으로 초기 값을 와이어링하지 않을 경우 시프트 레지스터는 호출간의 값을 기억합니다. LabVIEW 컴파일러는 시프트 레지스터로의 액세스를 효율적으로 처리합니다. 시프트 레지스터의 값을 읽어도 불필요하게 데이터 사본을 생성하지 않습니다. 사실 시프트 레지스터에 저장되어 있는 배열을 인덱싱하는 것은 물론 전체 배열의 추가 사본을 생성하지 않고도 값을 변경 및 업데이트할 수 있습니다. 시프트 레지스터의 문제는 시프트 레지스터가 있는 VI만 시프트 레지스터 데이터에 액세스할 수 있다는 것입니다. 한편, 시프트 레지스터에는 모듈화의 이점도 있습니다.

  

다음 그림에서와 같이 채널을 읽을지 변경할지 또는 제거할지 지정하거나 모든 채널에 대한 데이터를 제로로 설정할지 여부를 지정하는 모드 입력으로 단일 SubVI를 만들 수 있습니다. SubVI에는 2개의 시프트 레지스터가 있는 While 루프가 있는데, 하나는 채널 데이터용이며 하나는 채널 이름용입니다. 이 시프트 레지스터 중 어느 것도 초기화되지 않습니다. 그리고 나서 While 루프 내부에 모드 입력과 연결된 Case 구조를 놓습니다. 모드의 값에 따라 시프트 레지스터의 데이터를 읽고 변경할 수도 있습니다.
다음은 이 3가지 서로 다른 모드를 처리하는 인터페이스가 있는 SubVI의 개요입니다. Change Channel Info 코드만 표시되어 있습니다.

 

 1,000개의 원소에 대해 이러한 구현은 이전 구현보다 2배 빠르며 원래 구현보다는 4배 빠릅니다.

 

사례 연구 3 ‐ 문자열의 정적 글로벌 테이블

앞의 예에서는 테이블에 혼합 데이터 타입이 들어 있는 어플리케이션에 대해 살펴보았으며 이 테이블은 빈번히 변경될 수 있습니다. 대다수 어플리케이션에서는, 일단 생성이 되면 매우 정적인 정보 테이블이 생깁니다. 이 테이블은 스프레트시트 파일로부터 읽을 수 있습니다. 일단 메모리로 읽어오면 이를 활용하여 주로 정보를 찾을 수 있습니다. 이 경우, 구현은 2개의 함수, 즉 Initialize Table From File 및 Get Record From Table로 구성될 수 있습니다.

 

 

테이블을 구현하는 한 방법은 2D 배열의 문자열을 사용하는 것입니다. 컴파일러는 메모리의 별개 블록에 문자열 배열의 각 문자열을 저장한다는 점에 주의하십시오. 문자열이 많으면(예를 들어, 5,000개 이상의 문자열) 메모리 관리자에 로드를 놓을 수 있습니다. 이 로드는 개별 객체의 수가 증가함에 따라 성능에 현저한 손실을 야기시킬 수 있습니다. 큰 테이블을 저장하는 대체 방법은 단일 문자열로 테이블을 읽는 것입니다. 그리고 문자열의 각 레코드의 오프셋을 포함하는 별도의 배열을 작성합니다. 그러면 구성이 변경되므로 비교적 작은 메모리 블록 수천 개가 생기는 대신 하나의 큰 메모리 블록(문자열)과 별도의 작은 메모리 블록(오프셋 배열)만 생기게 됩니다. 이 방법은 구현하기에는 더 복잡하지만 큰 테이블의 경우에는 훨씬 빠를 수 있습니다.