29 메모리 모델 (Memory Model)

메모리 일관성 모델, 또는 메모리 모델은 SharedArrayBuffer를 백업으로 하는 TypedArray 인스턴스의 접근이나 Atomics 객체의 메서드를 통한 접근으로 발생한 Shared Data Block 이벤트들의 가능한 순서를 지정합니다. 프로그램에 데이터 레이스가 없는 경우(아래에 정의됨), 이벤트의 순서는 순차적 일관성(sequential consistency)을 갖습니다. 즉, 각 에이전트의 동작이 교차 배치된 것처럼 동작합니다. 프로그램에 데이터 레이스가 있는 경우에는, 공유 메모리 연산이 순차적으로 일관되지 않을 수 있습니다. 예를 들어, 프로그램이 인과관계를 위반하는 행동이나 기타 놀라운 현상을 보일 수 있습니다. 이런 놀라움은 컴파일러 변환 및 CPU의 설계(예: 순서 무시 실행이나 추측 실행)로 인해 발생합니다. 메모리 모델은 프로그램이 순차적으로 일관된 동작을 보이는 정확한 조건과 데이터 레이스로부터 읽히는 가능한 값들을 모두 정의합니다. 즉, 정의되지 않은 동작(undefined behaviour)은 없습니다.

메모리 모델은 SharedArrayBuffer에 대한 추상 연산이나 Atomics 객체의 메서드에 의해 평가 중 도입되는 Memory 이벤트들에 대한 관계적 제약으로 정의됩니다.

Note

이 절에서는 SharedArrayBuffer에 대한 추상 연산으로 도입된 Memory 이벤트에 대한 공리적(axiomatic) 모델을 제공합니다. 이 모델은 이 명세서의 나머지 부분과 달리 알고리즘으로 표현될 수 없다는 점을 강조해야 합니다. 추상 연산에 의한 이벤트의 비결정적 도입은 ECMAScript 평가의 연산 의미론과 메모리 모델의 공리 의미론 사이의 인터페이스입니다. 이 이벤트들의 의미론은 평가 내의 모든 이벤트 그래프를 고려함으로써 정의됩니다. 이것은 정적 의미론도 아니고 런타임 의미론도 아닙니다. 입증된 알고리즘 구현은 없고, 특정 이벤트 그래프가 허용되는지 아닌지를 결정하는 제약 조건 집합만 있습니다.

29.1 메모리 모델 기초 (Memory Model Fundamentals)

공유 메모리 접근(읽기/쓰기)은 아래에서 정의되는 두 그룹, atomic 접근과 data 접근으로 나뉜다. Atomic 접근은 순차적 일관성을 가지며, 즉 에이전트 클러스터의 모든 에이전트가 동의하는 엄격한 전체 순서가 있다. 비-atomic 접근은 모든 에이전트가 동의하는 엄격한 전체 순서가 없으므로 정렬되지(unordered) 않는다.

Note 1

순차적 일관성과 unordered 보다 약하거나 강한(예: release-acquire) 순서는 지원되지 않는다.

Shared Data Block 이벤트ReadSharedMemory, WriteSharedMemory, ReadModifyWriteSharedMemory 레코드 중 하나를 의미한다. 읽기 이벤트는 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 중 하나이고, 쓰기 이벤트는 WriteSharedMemory 또는 ReadModifyWriteSharedMemory 중 하나이다.

Table 94: ReadSharedMemory 이벤트 필드 (ReadSharedMemory Event Fields)
필드 이름 의미
[[Order]] seq-cst 또는 unordered 이 이벤트에 대해 메모리 모델이 보장하는 가장 약한 순서입니다.
[[NoTear]] Boolean 값 이 이벤트가 동일한 메모리 범위를 가지는 여러 쓰기 이벤트로부터 읽기를 허용하는지 여부입니다.
[[Block]] Shared Data Block 이벤트가 동작하는 블록입니다.
[[ByteIndex]] 0 이상의 정수 [[Block]] 안에서 읽기가 발생한 바이트 주소입니다.
[[ElementSize]] 0 이상의 정수 읽기 연산의 크기입니다.
Table 95: WriteSharedMemory 이벤트 필드 (WriteSharedMemory Event Fields)
필드 이름 의미
[[Order]] seq-cst, unordered, 또는 init 이 이벤트에 대해 메모리 모델이 보장하는 가장 약한 순서입니다.
[[NoTear]] Boolean 값 이 이벤트가 동일한 메모리 범위를 가진 여러 읽기 이벤트로부터 읽히는 것이 허용되는지 여부입니다.
[[Block]] Shared Data Block 이벤트가 동작하는 블록입니다.
[[ByteIndex]] 0 이상의 정수 [[Block]] 내에서 쓰기가 발생한 바이트 주소입니다.
[[ElementSize]] 0 이상의 정수 쓰기 연산의 크기입니다.
[[Payload]] 바이트 값들의 리스트 다른 이벤트에 의해 읽혀질 바이트 값들의 리스트입니다.
Table 96: ReadModifyWriteSharedMemory 이벤트 필드 (ReadModifyWriteSharedMemory Event Fields)
Field Name Value Meaning
[[Order]] seq-cst Read-modify-write 이벤트는 항상 순차적 일관성.
[[NoTear]] true Read-modify-write 이벤트는 tear 될 수 없다.
[[Block]] Shared Data Block 이벤트가 동작하는 블록.
[[ByteIndex]] 음이 아닌 정수 [[Block]] 내 read-modify-write 의 바이트 주소.
[[ElementSize]] 음이 아닌 정수 read-modify-write 의 크기.
[[Payload]] 바이트 값 List [[ModifyOp]] 에 전달될 바이트 값 List.
[[ModifyOp]] read-modify-write 수정 함수 읽은 바이트 List[[Payload]] 로부터 수정된 바이트 List 를 반환하는 추상 클로저.

Shared Data Block 이벤트는 추상 연산이나 Atomics 객체의 메서드를 통해 후보 실행 에이전트 이벤트 레코드에 도입됩니다. 일부 연산은 동기화(Synchronize) 이벤트도 도입하는데, 이 이벤트는 어떠한 필드도 가지지 않으며 오직 다른 이벤트의 허용된 순서를 직접적으로 제한하기 위해 존재합니다. 마지막으로 호스트 특화 이벤트도 존재합니다. 메모리 이벤트는 Shared Data Block 이벤트, 동기화(Synchronize) 이벤트, 혹은 이러한 호스트 특화 이벤트 중 하나입니다.

Shared Data Block 이벤트 e메모리 범위e.[[ByteIndex]](포함)부터 e.[[ByteIndex]] + e.[[ElementSize]](미포함)까지의 모든 정수의 집합이다. 두 이벤트의 메모리 범위는 [[Block]], [[ByteIndex]], [[ElementSize]]가 모두 동일할 때 같다. 두 이벤트의 메모리 범위는 [[Block]]이 같고, 범위가 같지 않으면서 교집합이 비어있지 않을 때 겹친다고 한다. 두 이벤트의 메모리 범위는 [[Block]]이 다르거나, 범위가 같지도 겹치지도 않을 때 서로 분리되어 있다고 한다.

Note 2

계정해야 할 호스트 특화 동기화 이벤트 예: 한 에이전트에서 다른 에이전트로 SharedArrayBuffer 를 보내기(브라우저의 postMessage), 에이전트 시작/종료, 공유 메모리 외 채널을 통한 에이전트 클러스터 내 통신. 특정 실행 execution 에 대해 이러한 이벤트는 host-synchronizes-with 엄격 부분 순서로 호스트에 의해 제공된다. 추가로, 호스트execution.[[EventList]]호스트 특화 동기화 이벤트를 추가하여 is-agent-order-before 관계에 참여할 수 있다.

이벤트는 아래에 정의된 관계에 의해 후보 실행 내에서 순서화된다.

29.2 에이전트 이벤트 레코드 (Agent Events Records)

Agent Events Record 는 다음 필드를 가진 Record 이다.

Table 97: 에이전트 이벤트 레코드 필드
필드 이름 의미
[[AgentSignifier]] 에이전트 식별자 이 순서가 만들어지게 된 평가를 수행한 에이전트입니다.
[[EventList]] 메모리 이벤트들의 리스트 이벤트는 평가 도중 리스트에 추가됩니다.
[[AgentSynchronizesWith]] 동기화 이벤트 쌍들의 리스트 연산 의미론에 의해 도입된 동기화 관계입니다.

29.3 Chosen Value 레코드 (Chosen Value Records)

Chosen Value Record 는 다음 필드를 가진 Record 이다.

Table 98: Chosen Value Record 필드 (Chosen Value Record Fields)
Field Name Value Meaning
[[Event]] Shared Data Block event 이 선택 값에 대해 도입된 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트.
[[ChosenValue]] 바이트 값 List 평가 동안 비결정적으로 선택된 바이트.

29.4 후보 실행 (Candidate Executions)

candidate execution 은 에이전트 클러스터 평가의 다음 필드를 가진 Record 이다.

Table 99: 후보 실행 레코드 필드
필드 이름 의미
[[EventsRecords]] 에이전트 이벤트 레코드들의 리스트 에이전트와 평가 도중 추가된 메모리 이벤트들의 리스트를 매핑합니다.
[[ChosenValues]] 선택된 값 레코드들의 리스트 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트를 평가 중 선택된 바이트 값들의 리스트에 매핑합니다.

empty candidate execution 은 필드가 빈 List 인 candidate execution Record 이다.

29.5 메모리 모델을 위한 추상 연산 (Abstract Operations for the Memory Model)

29.5.1 EventSet ( execution )

The abstract operation EventSet takes argument execution (후보 실행) and returns 메모리 이벤트들의 집합. It performs the following steps when called:

  1. events를 빈 집합으로 둔다.
  2. execution.[[EventsRecords]]의 각 에이전트 이벤트 레코드 aer에 대해,
    1. aer.[[EventList]]의 각 메모리 이벤트 E에 대해,
      1. Eevents에 추가한다.
  3. events를 반환한다.

29.5.2 SharedDataBlockEventSet ( execution )

The abstract operation SharedDataBlockEventSet takes argument execution (후보 실행) and returns Shared Data Block 이벤트들의 집합. It performs the following steps when called:

  1. events를 빈 집합으로 둔다.
  2. EventSet(execution)의 각 메모리 이벤트 E에 대해,
    1. EShared Data Block 이벤트라면, Eevents에 추가한다.
  3. events를 반환한다.

29.5.3 HostEventSet ( execution )

The abstract operation HostEventSet takes argument execution (후보 실행) and returns 메모리 이벤트들의 집합. It performs the following steps when called:

  1. EventSet(execution)에 있는 모든 요소 중 SharedDataBlockEventSet(execution)에 없는 요소만을 포함하는 새로운 집합을 반환한다.

29.5.4 ComposeWriteEventBytes ( execution, byteIndex, Ws )

The abstract operation ComposeWriteEventBytes takes arguments execution (후보 실행), byteIndex (0 이상의 정수), and Ws (WriteSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트들의 리스트) and returns 바이트 값들의 리스트. It performs the following steps when called:

  1. byteLocationbyteIndex로 둔다.
  2. bytesRead를 새 빈 리스트로 둔다.
  3. Ws의 각 요소 W에 대해,
    1. 단언: W메모리 범위byteLocation이 포함된다.
    2. payloadIndexbyteLocation - W.[[ByteIndex]]로 둔다.
    3. WWriteSharedMemory 이벤트라면,
      1. byteW.[[Payload]][payloadIndex]로 둔다.
    4. 아니면,
      1. 단언: WReadModifyWriteSharedMemory 이벤트이다.
      2. bytesValueOfReadEvent(execution, W)로 둔다.
      3. bytesModifiedW.[[ModifyOp]](bytes, W.[[Payload]])로 둔다.
      4. bytebytesModified[payloadIndex]로 둔다.
    5. bytebytesRead에 추가한다.
    6. byteLocationbyteLocation + 1로 업데이트한다.
  4. bytesRead를 반환한다.
Note 1

read-modify-write 변형 [[ModifyOp]]ReadModifyWriteSharedMemory 이벤트를 도입하는 Atomics 객체의 함수 프로퍼티에 의해 정해진다.

Note 2

이 추상 연산은 여러 쓰기 이벤트 리스트를 바이트 값 리스트로 합성한다. ReadSharedMemoryReadModifyWriteSharedMemory 이벤트의 이벤트 의미론에서 사용된다.

29.5.5 ValueOfReadEvent ( execution, R )

The abstract operation ValueOfReadEvent takes arguments execution (a candidate execution) and R (a ReadSharedMemory or ReadModifyWriteSharedMemory event) and returns a List of byte values. It performs the following steps when called:

  1. Wsexecution 에서 reads-bytes-from(R) 로 둔다.
  2. Assert: Ws 는 길이가 R.[[ElementSize]] 와 같은 WriteSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트 List 이다.
  3. ComposeWriteEventBytes(execution, R.[[ByteIndex]], Ws) 를 반환한다.

29.6 후보 실행의 관계 (Relations of Candidate Executions)

다음 관계(Relation) 및 수학 함수들은 특정 후보 실행을 기준으로 매개변수화되며, 해당 후보 실행의 메모리 이벤트를 순서화합니다.

29.6.1 is-agent-order-before

후보 실행 execution에 대하여, is-agent-order-before 관계는 아래 조건을 만족하는 메모리 이벤트 상의 최소 관계입니다.

  • 이벤트 E, D에 대하여, execution 내에서 ED보다 is-agent-order-before이면, execution.[[EventsRecords]]에 어떤 에이전트 이벤트 레코드 aer가 존재하여, aer.[[EventList]]ED 둘 다 포함하고, 리스트 순서상 ED보다 앞에 있음을 의미합니다.
Note

각 에이전트는 평가 과정에서 에이전트별로 엄격한 전체 순서로 이벤트를 도입합니다. 이는 그러한 엄격한 전체 순서들의 합집합이 됩니다.

29.6.2 reads-bytes-from

후보 실행 execution에 대하여, reads-bytes-from 함수는 SharedDataBlockEventSet(execution) 내의 메모리 이벤트SharedDataBlockEventSet(execution) 내 이벤트들의 리스트로 매핑하는 수학 함수이며, 다음 조건을 만족해야 합니다.

후보 실행에는 항상 reads-bytes-from 함수가 존재합니다.

29.6.3 reads-from

후보 실행 execution에 대하여, reads-from 관계는 아래 조건을 만족하는 메모리 이벤트 상의 최소 관계입니다.

29.6.4 host-synchronizes-with

후보 실행 execution에 대해, host-synchronizes-with 관계는 호스트 특화 메모리 이벤트 위의 호스트가 제공하는 엄격 부분 순서로서 최소한 다음을 만족해야 합니다.

  • Eexecution에서 host-synchronizes-with D이면, HostEventSet(execution)에 ED가 모두 존재합니다.
  • host-synchronizes-with와 is-agent-order-before의 합집합에는 순환(cycle)이 존재하지 않아야 합니다.
Note 1

후보 실행 execution 내 두 호스트 특화 이벤트 E, D에 대해, E host-synchronizes-with D이면 Eexecution에서 D보다 선행(happens-before)함을 의미합니다.

Note 2

이 관계는 호스트가 추가적인 동기화 수단(예: HTML 워커 사이의 postMessage)을 제공할 수 있게 해줍니다.

29.6.5 synchronizes-with

후보 실행 execution에 대해, synchronizes-with 관계는 다음을 만족하는 메모리 이벤트에 관한 최소 관계이다.

  • 이벤트 R, W에 대해, execution에서 RW로부터 reads-from이고, R.[[Order]]W.[[Order]]가 둘 다 seq-cst이며, RW메모리 범위가 같으면, W synchronizes-with R이다.
  • execution.[[EventsRecords]]의 각 요소 eventsRecord에 대해, 다음이 성립한다.
    • 이벤트 S, Sw에 대해, eventsRecord.[[AgentSynchronizesWith]]에 (S, Sw)가 포함되어 있으면 S synchronizes-with Sw이다.
  • 이벤트 E, D에 대해, execution.[[HostSynchronizesWith]]에 (E, D)가 있으면 E synchronizes-with D이다.
Note 1

메모리 모델 문헌의 관례에 따라, 후보 실행 execution에서는 write 이벤트가 read 이벤트와 synchronizes-with 관계를 맺으며, 반대는 성립하지 않는다.

Note 2

후보 실행 execution에서 init 이벤트는 이 관계에 참여하지 않으며 happens-before에 의해 직접 제한된다.

Note 3

후보 실행 execution에서 reads-from으로 연결된 모든 seq-cst 이벤트가 synchronizes-with로 연결되는 것은 아니다. 오직 메모리 범위가 같은 이벤트만 synchronizes-with로 연결된다.

Note 4

후보 실행 executionShared Data Block 이벤트 R, W에 대해, W synchronizes-with R이 성립하더라도 RW 외의 다른 write로부터 reads-from 관계를 가질 수 있다.

29.6.6 happens-before

후보 실행 execution에 대해, happens-before 관계는 다음을 만족하는 메모리 이벤트에 관한 최소 관계이다.

  • 이벤트 E, D에 대해, 아래 조건 중 하나라도 만족하면 execution에서 E happens-before D가 된다.

Note

happens-before는 agent-order의 상위 집합이므로, 후보 실행은 ECMAScript의 단일 스레드 평가 의미와 일치한다.

29.7 유효한 실행의 속성(Properties of Valid Executions)

29.7.1 유효한 선택 읽기(Valid Chosen Reads)

후보 실행 execution이 아래 알고리즘이 true를 반환하면 유효한 선택 읽기를 가진다.

  1. SharedDataBlockEventSet(execution)의 각 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트 R에 대해,
    1. execution.[[ChosenValues]]에서 [[Event]] 필드가 R인 요소를 chosenValueRecord로 둔다.
    2. chosenValueRecord.[[ChosenValue]]chosenValue로 둔다.
    3. ValueOfReadEvent(execution, R)을 readValue로 둔다.
    4. chosenValue의 요소 개수를 chosenLen으로 둔다.
    5. readValue의 요소 개수를 readLen으로 둔다.
    6. chosenLenreadLen이면
      1. false를 반환한다.
    7. 정수 i가 0(포함)에서 chosenLen(제외) 사이에 존재하여 chosenValue[i] ≠ readValue[i]이면
      1. false를 반환한다.
  2. true를 반환한다.

29.7.2 일관된 읽기(Coherent Reads)

후보 실행 execution이 아래 알고리즘이 true를 반환하면 일관된 읽기를 가진다.

  1. SharedDataBlockEventSet(execution)의 각 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트 R에 대해,
    1. execution에서 reads-bytes-from(R)을 Ws로 둔다.
    2. R.[[ByteIndex]]byteLocation으로 둔다.
    3. Ws의 각 요소 W에 대해,
      1. execution에서 R happens-before W이면
        1. false를 반환한다.
      2. WriteSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트 V가 존재하여, V메모리 범위byteLocation이 포함되고, execution에서 W happens-before V, V happens-before R이면
        1. false를 반환한다.
      3. byteLocationbyteLocation + 1로 설정한다.
  2. true를 반환한다.

29.7.3 찢김 없는 읽기(Tear Free Reads)

후보 실행 execution이 아래 알고리즘이 true를 반환하면 찢김 없는 읽기를 가진다.

  1. SharedDataBlockEventSet(execution)의 각 ReadSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트 R에 대해,
    1. R.[[NoTear]]true이면,
      1. R.[[ByteIndex]]R.[[ElementSize]]로 나눈 나머지가 0임을 단언한다.
      2. execution에서 Rreads-from W이고, W.[[NoTear]]true인 각 메모리 이벤트 W에 대해,
        1. RW메모리 범위가 같고, VW메모리 범위가 같으며, V.[[NoTear]]true이고, WV가 동일한 Shared Data Block 이벤트가 아니면서, execution에서 Rreads-from VV가 존재하면,
          1. false를 반환한다.
  2. true를 반환한다.
Note

Shared Data Block 이벤트[[NoTear]] 필드는 정수형 TypedArray 접근으로 도입된 경우 true, 부동소수점 TypedArray 또는 DataView로 도입된 경우 false이다.

이 요구사항은, 정렬된 방식(aligned)으로 정수형 TypedArray를 통해 접근하는 경우, 해당 범위의 하나의 쓰기(write) 이벤트만이 동등 범위의 다른 쓰기와의 데이터 경합(race)에서 "승리"해야 함을 의미한다. 보다 정확히는, 정렬된 읽기 이벤트는 서로 다른 여러 쓰기 이벤트의 바이트를 합쳐 읽는 것이 허용되지 않는다. 단, 정렬된 읽기가 겹치는 여러 쓰기 이벤트에서 읽는 것은 허용될 수 있다.

29.7.4 순차적 일관성 원자적 연산(Sequentially Consistent Atomics)

후보 실행 execution에 대해, is-memory-order-beforeEventSet(execution)에 포함된 모든 메모리 이벤트에 대한 엄격한 전체 순서(strict total order)이며, 다음을 만족해야 한다.

후보 실행이 is-memory-order-before 관계를 허용하면, 그 실행은 순차적 일관성 원자적 연산을 가진다.

Note 3

is-memory-order-before는 EventSet(execution)의 모든 이벤트를 포함하지만, happens-beforesynchronizes-with로 제한되지 않은 이벤트들은 순서 내에서 임의의 위치에 올 수 있다.

29.7.5 유효한 실행(Valid Executions)

다음이 모두 참이면 후보 실행 execution은 유효한 실행(또는 간단히 실행)이다.

모든 프로그램은 적어도 하나의 유효한 실행을 가진다.

29.8 경합(Races)

실행 executionSharedDataBlockEventSet(execution)에 포함된 이벤트 E, D에 대해, 아래 알고리즘이 true를 반환하면 E, D경합(race) 상태에 있다.

  1. E, D가 동일한 Shared Data Block 이벤트가 아니면
    1. execution에서 E happens-before D이고 D happens-before E가 모두 성립하지 않는 경우,
      1. E, D가 모두 WriteSharedMemory 또는 ReadModifyWriteSharedMemory 이벤트이고, E, D메모리 범위가 분리되어 있지 않으면
        1. true를 반환한다.
      2. execution에서 ED로부터 reads-from이거나, DE로부터 reads-from이면
        1. true를 반환한다.
  2. false를 반환한다.

29.9 데이터 경합(Data Races)

실행 executionSharedDataBlockEventSet(execution)에 포함된 이벤트 E, D에 대해, 아래 알고리즘이 true를 반환하면 E, D데이터 경합(data race) 상태에 있다.

  1. E, D경합(race)에 해당하면
    1. E.[[Order]]seq-cst이거나 D.[[Order]]seq-cst이면
      1. true를 반환한다.
    2. E, D메모리 범위가 겹치면
      1. true를 반환한다.
  2. false를 반환한다.

29.10 데이터 경합 없음(Data Race Freedom)

실행 execution에 대해, SharedDataBlockEventSet(execution) 내 두 이벤트가 데이터 경합(data race) 상태에 있지 않으면 데이터 경합 없음(data race free)이다.

모든 실행이 데이터 경합 없어야 프로그램이 데이터 경합 없음으로 간주된다.

메모리 모델은 데이터 경합이 없는 프로그램에서 모든 이벤트의 순차적 일관성을 보장한다.

29.11 공유 메모리 가이드라인(Shared Memory Guidelines)

Note 1

아래는 ECMAScript 프로그래머가 shared memory를 사용할 때 참고할 가이드라인이다.

프로그램이 data race free(데이터 경합 없음) 상태를 유지하도록, 즉 동일 메모리 위치에 대해 원자적이지 않은 연산이 동시에 발생할 수 없도록 작성할 것을 권한다. 데이터 경합이 없는 프로그램은 각 에이전트의 평가 의미 단계가 서로 interleave(교차)되는 의미론(interleaving semantics)을 가진다. 데이터 경합 없는 프로그램에서는 메모리 모델의 세부 사항을 이해할 필요가 없다. 그 세부 사항이 ECMAScript를 더 잘 쓰는 데 도움이 되는 직관을 쌓게 해 주지는 않는다.

보다 일반적으로, 프로그램이 data race free가 아니어도, 원자적 연산이 어떤 데이터 경합에도 참여하지 않고 경합 중인 모든 연산의 접근 크기가 같다면 예측 가능한 행동을 가질 수 있다. 원자적 연산이 경합에 참여하지 않도록 하려면, 원자적 연산과 비원자적 연산이 서로 다른 메모리 셀을 사용하도록 하며, 서로 다른 크기의 원자적 접근이 동시에 같은 메모리 셀에 접근하지 않음이 보장되게 해야 한다. 실제로 프로그래머는 공유 메모리를 최대한 강한 타입(strongly typed)처럼 다루는 것이 좋다. 여전히 경합 중인 비원자적 접근의 순서와 타이밍에는 의존할 수 없으나, 강하게 타입된 것처럼 다루면 중첩 읽기나 쓰기에 값이 "찢기는" 현상은 발생하지 않는다.

Note 2

아래는 공유 메모리를 사용하는 프로그램에서 컴파일러 변환을 구현하는 ECMAScript 구현체 대상 가이드라인이다.

단일 에이전트 환경에서 유효한 대부분의 프로그램 변환을 다중 에이전트 환경에서도 허용하는 것이 바람직하다. 이는 다중 에이전트 프로그램에서 각 에이전트의 성능이 단일 에이전트 환경에서만큼 좋게 유지될 수 있도록 보장하기 위함이다. 이러한 변환은 종종 판단하기 어렵다. 우리는 프로그램 변환에 대한 몇 가지 규칙을 개략적으로 제시한다. 이 규칙들은 규범적(nomative)으로 받아들여져야 하며(즉, 메모리 모델에 의해 암시되거나 메모리 모델이 암시하는 것보다 강한 규칙일 수 있다), 반드시 모든 경우를 망라하는 것은 아니다. 이러한 규칙들은 is-agent-order-before 관계를 구성하는 Memory 이벤트가 도입되기 이전의 프로그램 변환에 적용되는 것을 의도한다.

에이전트 순서 단편(agent-order slice)is-agent-order-before 관계 중 하나의 에이전트에 속한 부분 집합이다.

읽기 이벤트가능한 읽기 값(possible read values)은 그 이벤트에 대해 모든 유효 실행에서 ValueOfReadEvent로 얻을 수 있는 모든 값들의 집합이다.

공유 메모리가 없는 상황에서 허용되는 에이전트 순서 단편의 모든 변환은 공유 메모리가 있는 상황에서도 아래 예외를 제외하고 유효하다.

  • Atomics은 석판에 새겨진 것처럼 불변: 프로그램 변환은 [[Order]]seq-cstShared Data Block 이벤트is-agent-order-before 관계에서 제외하거나 서로 재배치(reorder)하거나, 또는 unordered 이벤트와의 상대적 순서를 에이전트 단편 내에서 재배치해서는 안 된다.

    (실제로 재배치 금지는 컴파일러가 모든 seq-cst 연산이 동기화이고 최종 is-memory-order-before 관계에 포함됨을 가정하게 만든다. 이는 크로스 에이전트 프로그램 분석이 없는 상황에서 보통 반드시 가정해야 하는 것이다. 또한, 컴파일러는 호출자의 메모리 순서 효과를 알 수 없는 호출이 seq-cst 연산을 포함할 수 있음을 항상 가정해야 한다.)

  • 읽기는 안정적이어야 한다: 동일한 shared memory read는 한 실행에서 하나의 값만을 관측해야 한다.

    (예를 들어, 의미상 단일 읽기인 부분이 프로그램 내에서 여러 번 실행되고, 그 각각에서 다른 값을 읽더라도, 프로그램은 결국 그 중 하나의 값만 관측할 수 있다. 이 규칙을 위반하는 변환을 rematerialization이라 한다.)

  • 쓰기도 안정적이어야 한다: shared memory에 대한 모든 관측 가능한 쓰기는 실행의 프로그램 의미에 따라야 한다.

    (예를 들어, 변환이 특정 관측 가능한 쓰기를 도입해서는 안 되는데, 더 큰 위치에 대해 read-modify-write 연산을 수행해 더 작은 데이터를 쓸 때, 프로그램이 쓸 수 없는 값을 메모리에 쓰거나, 방금 읽은 값을 그 위치에 다시 쓸 때 해당 위치가 그 사이에 다른 에이전트에 의해 덮어씌워질 수 있었다면 안 된다.)

  • 가능한 읽기 값은 반드시 비어 있지 않아야 한다: 프로그램 변환은 shared memory read의 가능한 읽기 값이 비워지게 해서는 안 된다.

    (직관에 반하게도, 이 규칙은 쓰기 변화에 대한 제약을 의미한다. 쓰기는 read 이벤트에 의해 읽힐 수 있기에 의미를 가진다. 예를 들어, 쓰기를 이동하거나 합치는 것은 가능하고, 때로는 두 seq-cst 연산 사이에서 재배열도 가능하지만, 각 위치를 갱신하는 모든 쓰기를 없애서는 안 된다. 일부 쓰기는 반드시 남아있어야 한다.)

여전히 유효한 변환의 예: 동일 위치에 대한 여러 비원자적 읽기의 병합, 비원자적 읽기들의 재배치, speculative(추측적) 비원자적 읽기 도입, 동일 위치에 대한 여러 비원자적 쓰기의 병합, 서로 다른 위치로의 비원자적 쓰기 재배열, 반복문 외부로의 비원자적 읽기 hoisting(끌어올리기, 결과적으로 종료 조건에 영향줌). 단, alias 되는 TypedArray의 존재로 인해 위치가 서로 다름을 입증하는 것이 일반적으로 어렵다는 점에 유의.

Note 3

아래는 공유 메모리 접근에 대한 기계어 코드 생성 시 ECMAScript 구현체 대상 가이드라인이다.

ARM이나 Power 이상의 메모리 모델을 가진 아키텍처에서는, 비원자적 저장 및 로드를 대상 아키텍처의 순수 저장/로드 명령어로 컴파일할 수 있다. 원자적 저장 및 로드는 순차적 일관성을 보장하는 명령어로 컴파일할 수 있다. 만약 그러한 명령어가 없다면, 기억 장벽(memory barrier)을 사용해야 하며, bare store/load 앞뒤에 배치해야 한다. read-modify-write 연산은 대상 아키텍처의 read-modify-write 명령으로 컴파일할 수 있다. (예: x86에서는 LOCK 프리픽스 명령, ARM에서는 load-exclusive/store-exclusive, Power에서는 load-link/store-conditional)

구체적으로, 메모리 모델은 다음과 같은 코드 생성을 허용하도록 의도되었다.

  • 프로그램의 모든 원자적 연산은 반드시 필요하다고 가정한다.
  • 원자적 연산은 서로 혹은 비원자적 연산과 재배열되지 않는다.
  • 함수는 항상 원자적 연산을 실행할 수 있다고 가정한다.
  • 원자적 연산은 더 큰 데이터에 대한 read-modify-write 연산으로 구현되지 않고, 해당 크기의 원자적 연산이 플랫폼에서 lock-free가 아니면 비-lock-free 원자적으로 구현된다. (관심 있는 모든 크기의 일반 메모리 접근이 모든 플랫폼에 있다고 이미 가정한다.)

가장 단순한 naive 코드 생성 패턴은 다음과 같다:

  • 일반 load/store는 각각 단일 load/store 명령으로 변환된다.
  • lock-free atomic load/store는 전체 fence, 일반 load/store, 전체 fence로 변환된다.
  • lock-free atomic read-modify-write는 전체 fence, atomic read-modify-write 명령 시퀀스, 전체 fence로 변환된다.
  • 비-lock-free atomics는 spinlock 획득, 전체 fence, 일련의 비원자적 load/store, 전체 fence, spinlock 해제 순이다.

이는 atomic 연산이 비원자적 쓰기, 혹은 다른 크기의 atomic 연산과 경합하지 않을 때 올바르다. 메모리 모델은 경합한 atomic 연산의 원자성(atomicity)을 효과적으로 비원자적으로 강등시킨다. 반면, naive 매핑은 매우 강력해서 atomic 연산 자체가 순차적 일관성 fence로 사용될 수 있지만 실제로 메모리 모델이 이를 보장하지는 않는다.

메모리 모델의 제약 하에, 이 기본 패턴에 대한 국소적 최적화도 허용된다. 예:

  • 플랫폼 특유의 fence 최적화가 가능하다. 예를 들어 x86에서는 atomic load/store 앞뒤의 fence를 항상 생략 가능(단, store 이후 fence는 예외), lock-free atomic read-modify-write에는 fence가 필요 없다. 많은 플랫폼에는 여러 단계의 fence가 있고, 상황에 따라 더 약한 fence도 사용할 수 있다.
  • 현대 플랫폼 대부분은 ECMAScript atomics에서 요구하는 모든 데이터 크기에 대해 lock-free atomics를 지원한다. 비-lock-free atomics가 필요한 경우 atomic 바디를 감싸는 fence는 lock/unlock 단계에 absorb(흡수)될 수 있다. 가장 간단한 방식은 SharedArrayBuffer당 하나의 lock word를 운영하는 것.
  • 좀 더 복잡한 플랫폼 특유 최적화도 가능하다. 예를 들어 두 fence가 연속된다면 하나로 합칠 수 있다. atomic 연산이 연속된다면 그 사이에 하나의 fence만 두면 충분하다. x86에서는 atomic store 뒤의 fence조차도 생략 가능(이는 store 이후 다음 load를 보호하기 위해 필요).