모든 이야기

혼자 값을 갱신하는 레지스터도 합성에서 사라질 수 있다

디버깅을 위해 RTL에 계산을 하나 더 넣었는데 합성 후에는 흔적이 없다. 분명 매 클록 값을 갱신하는 레지스터까지 있었는데, 합성기가 잘못 지운 것일까? 그 값이 아무 관측 동작에도 영향을 주지 않았다면 제거할 수 있다. dead logic 제거는 계산이 실행되는 빈도가 아니라, 보존해야 할 결과로 이어지는 경로가 있는지를 따진다.

스스로 값을 읽는 레지스터도 사라질 수 있다

8비트 카운터 count와 16비트 누적값 scratch가 있다고 하자. 둘은 같은 동기식 리셋으로 0이 된다. count만 출력 포트 y로 내보내고, scratch는 다른 곳에 연결하지 않는다.

always @(posedge clk) begin
  if (rst) begin
    count   <= 8'd0;
    scratch <= 16'd0;
  end else begin
    count   <= count + 8'd1;
    scratch <= scratch + count;
  end
end
assign y = count;

scratch는 매번 자기 값을 읽어 다음 값을 만든다. 따라서 “아무도 읽지 않는 신호만 지우면 된다”는 단순한 기준으로 보면 scratch는 사용 중이다. 하지만 scratch와 그 덧셈기 안에서만 값이 돌고, y로 나가는 경로는 없다. 보존해야 할 외부 출력의 관점에서 보면 이 고리는 결과에 영향을 주지 않는다.

Yosys의 opt_clean 설명은 쓰지 않는 셀과 선을 제거한다고 정한다. 실제 구현은 출력 포트나 keep으로 유지할 신호 등에서 필요한 드라이버를 찾고, 그 셀의 입력을 거슬러 올라가며 사용되는 회로를 추적한다. 스스로 연결된 로직이라도 그 관측 경계와 이어지지 않으면 필요한 회로의 집합에 들어오지 않을 수 있다. opt_clean 명령의 공식 설명, 출력에서 입력 회로를 추적하는 구현

위 예에서 count와 그 증가 경로는 y에 필요하다. scratch의 다음 값을 계산할 때 count를 사용하지만, 이는 count가 scratch에 의존한다는 뜻이 아니다. scratch 쪽을 지워도 count의 계산에는 아무것도 빠지지 않는다. 의존성의 방향을 반대로 읽지 않는 것이 중요하다.

같은 파형과 달라진 관측 경계

리셋 뒤 네 번의 정상 에지를 따라가면 count는 1, 2, 3, 4가 된다. scratch는 이전 count를 더하므로 0, 1, 3, 6이 된다. scratch 회로를 모두 지운 버전에서도 y는 1, 2, 3, 4다. 보존할 출력이 y뿐인 이 가상 설계에서는 두 버전의 관측 결과가 같다.

그러나 assign debug = scratch라는 새 출력 포트를 추가하면 이야기가 바뀐다. 이제 같은 네 에지에서 debug=0, 1, 3, 6도 외부에 보고해야 한다. scratch의 레지스터와 덧셈 경로는 그 출력을 만들기 위해 필요하다. 디버그 포트 하나가 단순히 이름을 늘리는 것이 아니라, 기능 보존에서 관측해야 할 결과의 집합을 늘린 것이다.

이 예의 값은 비차단 대입에 따른 상태 전이를 계산한 것이며, 실제 합성기가 몇 개의 셀을 지웠다는 측정값은 아니다. 어떤 셀로 매핑되는지와 비용은 별도 결과를 봐야 한다. 하지만 출력 의존성을 분리해 계산하는 것만으로 제거가 기능적으로 허용되는 이유는 확인할 수 있다.

내부 신호에 이름이 있다는 사실도 외부 결과와 같지 않다. 읽기 좋은 공개 이름의 선을 정리에서 남기는 옵션이나 동작이 있더라도, 그 이름만으로 모든 드라이버 회로의 보존을 기대해서는 안 된다. 관측 포트, 도구가 지원하는 keep 속성, 디버그 삽입 흐름의 요구를 실제 설정에 맞춰 사용해야 한다. opt_clean의 -purge는 공개 이름을 가진 내부 선의 정리 범위에도 영향을 준다.

조건을 정리한 뒤에야 죽는 회로

직접 출력으로 연결돼 있어도 상수 선택 때문에 더 이상 사용되지 않을 수 있다. y = 1'b0 ? scratch : count라면 상수 선택을 정리한 뒤 scratch 경로는 y에 닿지 않는다. 앞의 패스가 선을 재연결하고 뒤의 정리 패스가 남은 셀을 지우는 식으로 결과가 단계적으로 나타날 수 있다.

공식 최적화 설명은 opt_expr가 상수 표현식 등을 정리하고, opt_clean이 남은 사용하지 않는 선과 셀을 제거하는 역할을 구분한다. 여러 패스의 호출 순서에 따라 중간 표현에는 아직 사용하지 않는 셀이 남아 있을 수 있다. 중간 netlist에 보인다는 이유로 최종 실리콘에도 반드시 남는다고 판단하기 어려운 이유다. Yosys 최적화 흐름

제거 가능성을 따질 때는 보존할 출력과 특별히 유지할 셀의 경계를 먼저 정한다. 메모리 쓰기나 다른 장치로 나가는 제어처럼 외부 효과가 있다면 그 효과도 관측 동작에 포함돼야 한다. 단순한 덧셈 결과와 같은 기준으로 경계 밖의 부작용을 잊어서는 안 된다. 불투명한 모듈이나 도구가 처리하는 특수 셀의 보존 규칙도 해당 흐름을 따라야 한다.

사라진 디버그 계산이 필요한 기능이었다면 먼저 연결이나 보존 설정을 확인하면 된다. 필요한 출력에 영향을 주지 않는 보조 계산이었다면 제거는 기능을 유지한 채 구현을 줄인 결과다. RTL에 오래 존재했는지보다, 그 계산이 무엇을 관측 가능하게 만들었는지를 따라가면 합성 결과의 빈자리를 이해할 수 있다.